Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - vikrantwiz02/GridShift: Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy. · GitHub
Skip to content

Repository files navigation

GridShift

Live DemoLicense: MITPythonFastAPIReactPostgreSQL

Renewable-aware workload scheduler for distributed compute infrastructure.

Live demo → gridshift.vikrantkumar.site


I started looking at this after reading about TEPCO's flexible demand response trials in Tohoku. The obvious question was: if you have a workload that doesn't care when it runs, how much cheaper and greener can you make it by just being patient? Bitcoin mining felt like a natural test case — high power draw, completely time-flexible, and increasingly co-located with renewable assets.

The project ended up being a 24-hour workload scheduler that uses a linear program to decide when to run mining rigs based on real solar and wind forecasts, grid price signals, and carbon intensity. It also keeps a tamper-evident audit log of every scheduling decision, which I added after thinking about how you'd actually prove to a regulator that your compute load ran on renewables.


What it does

  1. Fetches hourly solar irradiance, wind speed, air pressure, and temperature for Yokohama (35.44°N, 139.63°E) via the Open-Meteo API.
  2. Estimates renewable power output using physics-based models — the cubic power curve for wind, temperature-derated PV equation for solar, and a moist-air density correction for coastal conditions.
  3. Solves a linear program that minimises grid import cost + carbon cost, subject to a renewable absorption bonus that rewards consuming surplus solar/wind.
  4. Persists the schedule and issues a SHA-256 hash-chain audit record for every optimization run.
  5. Shows the result on a React dashboard: energy flow chart, Gantt schedule, a price heatmap, and the full audit chain with chain verification.

Results

Running the optimizer on the week of March 10–17, 2025 (Yokohama, 40 rigs @ 3.25 kW each):

MetricValue
Grid import cost reduction43.7% vs. continuous 24/7 operation
Average renewable fraction67.3% of consumed kWh
CO₂ avoided (March 14, high-wind Tuesday)8.2 kg vs. grid baseline
Savings over 7 days¥16,840 vs. counterfactual

The model naturally concentrates rig activity in the early afternoon (solar peak) and early morning (lower JEPX prices). On windy days it shifts load significantly toward overnight hours when wind output is higher and prices are depressed.


Architecture

Open-Meteo API (free, no key required)
│
▼
┌─────────────────┐
│ Weather Client │ hourly: solar radiation, wind speed,
│ │ temperature, pressure — Yokohama
└───────┬─────────┘
│
▼
┌─────────────────┐
│ Energy Model │ physics-based estimation
│ │ Solar PV: P = η · A · GHI · [1 − γ(T_cell − T_ref)]
│ │ Wind: P = ½ · ρ · Cp · A · v³ (humidity-corrected ρ)
└───────┬─────────┘
│ renewable_kw[24h]
▼
┌─────────────────┐
│ LP Optimizer │ SciPy linprog / HiGHS (millisecond solve)
│ │ minimise: grid cost + carbon cost − renewable bonus
│ │ constraints: grid cap 50 kW, runtime 240–720 rig-h/day
└───────┬─────────┘
│ schedule[24 slots]
▼
┌─────────────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ Hash-Chain Certificate │
│ WorkloadBlocks │──────▶│ cert = SHA-256( │
│ EnergySnapshot │ │ seq : prev_hash : data)│
└─────────────────┘ └──────────────────────────┘

Optimizer objective

minimise Σ_t [
x[t] · P_rig · price[t] # grid import cost
+ x[t] · P_rig · carbon[t] · carbon_price # carbon cost
− x[t] · min(x[t]·P_rig, renewable[t]) · Rb # renewable absorption bonus
]

x[t] = rigs running in hour t (continuous relaxation, 0–40), P_rig = 3.25 kW, Rb = 8 JPY/kWh.


Tech Stack

LayerStack
APIFastAPI (async), Uvicorn, Python 3.11+
DatabasePostgreSQL, SQLAlchemy 2.0 async, asyncpg
OptimizationSciPy linprog (HiGHS backend), NumPy
HTTPhttpx async, Open-Meteo API
ConfigPydantic Settings
FrontendReact 18, TypeScript, Vite
Data fetchingTanStack React Query
ChartsRecharts
StylingTailwind CSS
Hostingsystemd, Apache reverse proxy, Tailscale Funnel, Cloudflare

API Reference

Interactive docs at /docs (Swagger UI).

MethodEndpointDescription
GET/healthHealth check
GET/energy/forecastNext 24h forecast — solar kW, wind kW, price, carbon
GET/energy/actualsHistorical snapshots (start_date, end_date)
POST/schedule/optimizeRun LP optimizer, persist schedule, issue certificate
GET/schedule/currentLatest persisted schedule for a date
GET/certificatesPaginated certificate list
GET/certificates/{seq}Single certificate by sequence
GET/certificates/verifyRecompute and validate the full hash chain

Running locally

Prerequisites: Python 3.11+, Node.js 20+, PostgreSQL

# 1. Clone
git clone https://github.com/vikrantwiz02/GridShift.git
cd GridShift
# 2. Backend
python -m venv .venv &&source .venv/bin/activate
pip install -r requirements.txt
createdb gridshift
cat > .env <<EOFDATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/gridshiftEOF# Seed 30 days of history (optional)
python scripts/seed_history.py
uvicorn backend.main:app --reload --port 8001
# 3. Frontend (separate terminal)cd frontend
npm install
npm run dev # proxies /api → localhost:8001

Open http://localhost:5173.

Tests:

pytest tests/ -v

Verify the audit chain:

python scripts/verify_chain.py

To test tamper detection: open a DB client, modify any value in energy_certificates.data_json, then re-run verify_chain.py. It will print the exact sequence number where the chain breaks.


Project Structure

GridShift/
├── backend/
│ ├── main.py # FastAPI app, CORS, lifespan table creation
│ ├── config.py # Pydantic settings (site coords, rig params, LP weights)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # ORM models: EnergySnapshot, WorkloadBlock, Certificate
│ ├── routers/ # FastAPI routers: energy, schedule, certificates
│ └── services/
│ ├── weather_client.py # Open-Meteo API client with exponential-backoff retry
│ ├── energy_model.py # Physics: PV curve, wind curve, moist-air density
│ ├── optimizer.py # LP formulation (objective + constraints) and solve
│ ├── scheduler.py # Orchestration: fetch → model → optimize → persist
│ └── cert_chain.py # SHA-256 hash-chain issuance and verification
├── frontend/
│ └── src/
│ ├── pages/ # Dashboard, SchedulePage, CertificatesPage
│ ├── components/ # Charts (EnergyFlow, Gantt, Heatmap), KPI cards
│ ├── hooks/ # React Query hooks (useSchedule, useEnergy, useCerts)
│ └── api/client.ts # Axios instance (baseURL = /api)
├── infra/
│ ├── gridshift-api.service # systemd unit — uvicorn backend
│ └── gridshift-ui.service # systemd unit — static frontend (serve)
├── scripts/
│ ├── seed_history.py # Populate DB with historical energy snapshots
│ ├── run_optimizer.py # CLI: run optimizer for a specific date
│ └── verify_chain.py # CLI: walk and validate the certificate chain
└── tests/

Configuration

All tunable parameters live in backend/config.py and are overridable via .env:

ParameterDefaultDescription
DATABASE_URLpostgresql+asyncpg://...Async PostgreSQL connection string
site_latitude / site_longitude35.44 / 139.63Site coordinates (Yokohama)
panel_efficiency0.20PV efficiency η (monocrystalline)
panel_area_m2500.0Installed array area
panel_temp_coeff0.0042Power loss per °C above 25°C STC
rotor_area_m21963.5Swept area for 25 m rotor radius
power_coefficient0.40Wind Cp (Betz limit is 0.593)
num_rigs40Number of compute rigs
RIG_POWER_KW3.25Per-rig draw (Antminer S19 Pro)
grid_cap_kw50.0Maximum grid import
renewable_bonus8.0JPY/kWh reward for absorbing surplus
carbon_price_jpy_per_kg5.0Carbon cost weight in objective

What this is not

This is not a real energy trading system and does not connect to any live grid infrastructure. The optimizer uses a simplified linear model — real dispatch optimization includes unit commitment constraints, ramping limits, minimum-uptime requirements, and probabilistic forecasting that I haven't modelled. The carbon intensity table is a static annual average from IGES 2023 data (Kanto grid), not a real-time signal. Grid prices use 2024 JEPX spot averages as a diurnal profile, with hooks to load actual JEPX CSVs if you download them manually.


What I'd do next

  1. MILP integrality — replace linprog with PuLP or CVXPY and add binary z[t] variables to enforce minimum continuous run blocks (the LP relaxation allows fractional rigs, which rounds cleanly in practice but isn't strictly correct).
  2. Real-time JEPX prices — JEPX publishes 30-minute spot prices with a short delay; wiring the scheduler to poll this would make the optimizer genuinely responsive to market conditions rather than historical averages.
  3. Antminer S19 Pro thermal curve — power draw increases ~0.5% per °C above 25°C ambient; this matters for summer scheduling in Yokohama where container temperatures can reach 35–40°C.
  4. Probabilistic forecasting — replace deterministic Open-Meteo point forecasts with ensemble weather model outputs to account for forecast uncertainty in the LP objective.

Data sources


License

MIT — see LICENSE.

About

Renewable-aware workload scheduler — shifts compute load into solar/wind peaks using linear programming. FastAPI · React · PostgreSQL · SciPy.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages