viewof contextEnergyOptimizer = {
// ===========================================================================
// CONTEXT-AWARE ENERGY OPTIMIZER
// ===========================================================================
// Demonstrates adaptive power management based on:
// - Time of day (activity patterns)
// - Motion detection (presence sensing)
// - Battery level (energy-aware adaptation)
// - Network quality (transmission optimization)
// - Data urgency (priority-based scheduling)
//
// IEEE Color Palette:
// Navy: #2C3E50 (primary, headers)
// Teal: #16A085 (secondary, efficient)
// Orange: #E67E22 (highlights, warnings)
// Gray: #7F8C8D (neutral, inactive)
// LtGray: #ECF0F1 (backgrounds)
// Green: #27AE60 (savings, positive)
// Red: #E74C3C (consumption, negative)
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 950,
height: 950,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6",
blue: "#3498DB"
},
// Power consumption profiles (mA)
power: {
mcu: {
active: 15,
idle: 0.5,
sleep: 0.001
},
sensor: {
sampling: 5,
idle: 0.01
},
radio: {
tx: 40,
rx: 20,
idle: 0.002
},
processing: {
local: 25,
edge: 5, // Just transmission
cloud: 3 // Minimal local
}
},
// Voltage
voltage: 3.3,
// Battery capacity (mAh)
batteryCapacity: 2000
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
// Context inputs
timeOfDay: 12, // 0-24 hours
motionDetected: true,
batteryLevel: 80, // 0-100%
networkQuality: "good", // poor, fair, good
dataUrgency: "medium", // low, medium, high
// Strategy configuration
samplingRate: 10, // seconds
txFrequency: 60, // seconds
processingLocation: "local", // local, edge, cloud
// Mode toggle
adaptiveMode: true,
// Simulation state
isSimulating: false,
simulationHour: 0,
energyProfile: [],
staticProfile: [],
adaptiveProfile: []
};
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// CONTEXT INPUTS PANEL
// ---------------------------------------------------------------------------
const contextPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.gray}`);
contextPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Context Inputs");
const contextGrid = contextPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(170px, 1fr))")
.style("gap", "15px");
// Time of Day Slider
const timeGroup = contextGrid.append("div");
timeGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Time of Day");
const timeSlider = timeGroup.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", 24)
.attr("step", 0.5)
.attr("value", state.timeOfDay)
.style("width", "100%")
.style("cursor", "pointer");
const timeValue = timeGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("font-size", "12px")
.style("margin-top", "3px");
const timeDisplay = timeValue.append("span")
.style("color", config.colors.navy)
.style("font-weight", "bold")
.text(formatTime(state.timeOfDay));
const timeIcon = timeValue.append("span")
.text(getTimeIcon(state.timeOfDay));
timeSlider.on("input", function() {
state.timeOfDay = +this.value;
timeDisplay.text(formatTime(state.timeOfDay));
timeIcon.text(getTimeIcon(state.timeOfDay));
updateCalculations();
});
function formatTime(hour) {
const h = Math.floor(hour);
const m = (hour % 1) * 60;
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
function getTimeIcon(hour) {
if (hour >= 6 && hour < 12) return "Morning";
if (hour >= 12 && hour < 18) return "Afternoon";
if (hour >= 18 && hour < 22) return "Evening";
return "Night";
}
// Motion Detection Toggle
const motionGroup = contextGrid.append("div");
motionGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Motion Detected");
const motionToggle = motionGroup.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("align-items", "center");
const motionBtn = motionToggle.append("button")
.style("padding", "8px 16px")
.style("background", state.motionDetected ? config.colors.green : config.colors.gray)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "20px")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.style("transition", "background 0.3s")
.text(state.motionDetected ? "Active" : "Inactive")
.on("click", function() {
state.motionDetected = !state.motionDetected;
d3.select(this)
.style("background", state.motionDetected ? config.colors.green : config.colors.gray)
.text(state.motionDetected ? "Active" : "Inactive");
updateCalculations();
});
// Battery Level Slider
const batteryGroup = contextGrid.append("div");
batteryGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Battery Level");
const batterySlider = batteryGroup.append("input")
.attr("type", "range")
.attr("min", 5)
.attr("max", 100)
.attr("value", state.batteryLevel)
.style("width", "100%")
.style("cursor", "pointer");
const batteryDisplay = batteryGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-top", "3px");
const batteryValue = batteryDisplay.append("span")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("color", getBatteryColor(state.batteryLevel))
.text(`${state.batteryLevel}%`);
const batteryBar = batteryDisplay.append("div")
.style("width", "60px")
.style("height", "16px")
.style("border", `2px solid ${config.colors.gray}`)
.style("border-radius", "3px")
.style("overflow", "hidden")
.style("position", "relative");
batteryBar.append("div")
.attr("class", "battery-fill")
.style("width", `${state.batteryLevel}%`)
.style("height", "100%")
.style("background", getBatteryColor(state.batteryLevel))
.style("transition", "width 0.3s, background 0.3s");
batterySlider.on("input", function() {
state.batteryLevel = +this.value;
batteryValue.text(`${state.batteryLevel}%`)
.style("color", getBatteryColor(state.batteryLevel));
batteryBar.select(".battery-fill")
.style("width", `${state.batteryLevel}%`)
.style("background", getBatteryColor(state.batteryLevel));
updateCalculations();
});
function getBatteryColor(level) {
if (level > 50) return config.colors.green;
if (level > 20) return config.colors.orange;
return config.colors.red;
}
// Network Quality Selector
const networkGroup = contextGrid.append("div");
networkGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Network Quality");
const networkSelect = networkGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer");
[
{ value: "poor", label: "Poor (High retry)", color: config.colors.red },
{ value: "fair", label: "Fair (Some retry)", color: config.colors.orange },
{ value: "good", label: "Good (Low retry)", color: config.colors.green }
].forEach(opt => {
networkSelect.append("option")
.attr("value", opt.value)
.attr("selected", opt.value === state.networkQuality ? true : null)
.text(opt.label);
});
networkSelect.on("change", function() {
state.networkQuality = this.value;
updateCalculations();
});
// Data Urgency Selector
const urgencyGroup = contextGrid.append("div");
urgencyGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Data Urgency");
const urgencySelect = urgencyGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer");
[
{ value: "low", label: "Low (Batch OK)" },
{ value: "medium", label: "Medium (Regular)" },
{ value: "high", label: "High (Immediate)" }
].forEach(opt => {
urgencySelect.append("option")
.attr("value", opt.value)
.attr("selected", opt.value === state.dataUrgency ? true : null)
.text(opt.label);
});
urgencySelect.on("change", function() {
state.dataUrgency = this.value;
updateCalculations();
});
// ---------------------------------------------------------------------------
// STRATEGY CONFIGURATION PANEL
// ---------------------------------------------------------------------------
const strategyPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.gray}`);
strategyPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Strategy Configuration");
const strategyGrid = strategyPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
// Sampling Rate
const sampleGroup = strategyGrid.append("div");
sampleGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Sampling Rate");
const sampleSlider = sampleGroup.append("input")
.attr("type", "range")
.attr("min", 1)
.attr("max", 60)
.attr("value", state.samplingRate)
.style("width", "100%")
.style("cursor", "pointer");
const sampleDisplay = sampleGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("font-size", "12px")
.style("margin-top", "3px");
const sampleValue = sampleDisplay.append("span")
.style("color", config.colors.teal)
.style("font-weight", "bold")
.text(`${state.samplingRate} sec`);
const sampleLabel = sampleDisplay.append("span")
.style("color", config.colors.gray)
.text(state.samplingRate <= 5 ? "(Aggressive)" : state.samplingRate <= 30 ? "(Normal)" : "(Conservative)");
sampleSlider.on("input", function() {
state.samplingRate = +this.value;
sampleValue.text(`${state.samplingRate} sec`);
sampleLabel.text(state.samplingRate <= 5 ? "(Aggressive)" : state.samplingRate <= 30 ? "(Normal)" : "(Conservative)");
updateCalculations();
});
// Transmission Frequency
const txGroup = strategyGrid.append("div");
txGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Transmission Frequency");
const txSlider = txGroup.append("input")
.attr("type", "range")
.attr("min", 10)
.attr("max", 300)
.attr("step", 10)
.attr("value", state.txFrequency)
.style("width", "100%")
.style("cursor", "pointer");
const txDisplay = txGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("font-size", "12px")
.style("margin-top", "3px");
const txValue = txDisplay.append("span")
.style("color", config.colors.orange)
.style("font-weight", "bold")
.text(`${state.txFrequency} sec`);
const txLabel = txDisplay.append("span")
.style("color", config.colors.gray)
.text(`(${(3600 / state.txFrequency).toFixed(0)} tx/hour)`);
txSlider.on("input", function() {
state.txFrequency = +this.value;
txValue.text(`${state.txFrequency} sec`);
txLabel.text(`(${(3600 / state.txFrequency).toFixed(0)} tx/hour)`);
updateCalculations();
});
// Processing Location
const procGroup = strategyGrid.append("div");
procGroup.append("label")
.style("display", "block")
.style("margin-bottom", "5px")
.style("color", config.colors.navy)
.style("font-weight", "600")
.style("font-size", "13px")
.text("Processing Location");
const procSelect = procGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer");
[
{ value: "local", label: "Local (On-device)" },
{ value: "edge", label: "Edge (Gateway)" },
{ value: "cloud", label: "Cloud (Server)" }
].forEach(opt => {
procSelect.append("option")
.attr("value", opt.value)
.attr("selected", opt.value === state.processingLocation ? true : null)
.text(opt.label);
});
procSelect.on("change", function() {
state.processingLocation = this.value;
updateCalculations();
});
// Mode Toggle and Presets Row
const modeRow = strategyPanel.append("div")
.style("display", "flex")
.style("gap", "15px")
.style("margin-top", "15px")
.style("flex-wrap", "wrap")
.style("align-items", "center");
// Static vs Adaptive Toggle
const modeToggle = modeRow.append("div")
.style("display", "flex")
.style("background", config.colors.white)
.style("border-radius", "25px")
.style("overflow", "hidden")
.style("border", `2px solid ${config.colors.gray}`);
const staticBtn = modeToggle.append("button")
.style("padding", "10px 20px")
.style("border", "none")
.style("background", !state.adaptiveMode ? config.colors.navy : config.colors.white)
.style("color", !state.adaptiveMode ? config.colors.white : config.colors.navy)
.style("font-size", "13px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Static")
.on("click", function() {
state.adaptiveMode = false;
updateModeToggle();
updateCalculations();
});
const adaptiveBtn = modeToggle.append("button")
.style("padding", "10px 20px")
.style("border", "none")
.style("background", state.adaptiveMode ? config.colors.teal : config.colors.white)
.style("color", state.adaptiveMode ? config.colors.white : config.colors.navy)
.style("font-size", "13px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Adaptive")
.on("click", function() {
state.adaptiveMode = true;
updateModeToggle();
updateCalculations();
});
function updateModeToggle() {
staticBtn
.style("background", !state.adaptiveMode ? config.colors.navy : config.colors.white)
.style("color", !state.adaptiveMode ? config.colors.white : config.colors.navy);
adaptiveBtn
.style("background", state.adaptiveMode ? config.colors.teal : config.colors.white)
.style("color", state.adaptiveMode ? config.colors.white : config.colors.navy);
}
// Scenario Presets
modeRow.append("span")
.style("color", config.colors.gray)
.style("font-size", "13px")
.style("margin-left", "auto")
.text("Presets:");
const presets = [
{ name: "Office Hours", time: 10, motion: true, battery: 80, network: "good", urgency: "medium" },
{ name: "Night Mode", time: 2, motion: false, battery: 60, network: "good", urgency: "low" },
{ name: "Low Battery", time: 14, motion: true, battery: 15, network: "fair", urgency: "low" },
{ name: "High Activity", time: 16, motion: true, battery: 90, network: "good", urgency: "high" }
];
presets.forEach(preset => {
modeRow.append("button")
.style("padding", "8px 12px")
.style("background", config.colors.white)
.style("color", config.colors.navy)
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("font-size", "11px")
.style("cursor", "pointer")
.text(preset.name)
.on("click", () => applyPreset(preset))
.on("mouseenter", function() {
d3.select(this).style("background", config.colors.lightGray);
})
.on("mouseleave", function() {
d3.select(this).style("background", config.colors.white);
});
});
function applyPreset(preset) {
state.timeOfDay = preset.time;
state.motionDetected = preset.motion;
state.batteryLevel = preset.battery;
state.networkQuality = preset.network;
state.dataUrgency = preset.urgency;
// Update UI
timeSlider.property("value", preset.time);
timeDisplay.text(formatTime(preset.time));
timeIcon.text(getTimeIcon(preset.time));
motionBtn
.style("background", preset.motion ? config.colors.green : config.colors.gray)
.text(preset.motion ? "Active" : "Inactive");
batterySlider.property("value", preset.battery);
batteryValue.text(`${preset.battery}%`).style("color", getBatteryColor(preset.battery));
batteryBar.select(".battery-fill")
.style("width", `${preset.battery}%`)
.style("background", getBatteryColor(preset.battery));
networkSelect.property("value", preset.network);
urgencySelect.property("value", preset.urgency);
updateCalculations();
}
// ---------------------------------------------------------------------------
// ENERGY PROFILE VISUALIZATION
// ---------------------------------------------------------------------------
const chartHeight = 280;
const chartMargin = { top: 40, right: 30, bottom: 50, left: 60 };
const chartPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "15px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.gray}`);
chartPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("24-Hour Energy Profile");
const svg = chartPanel.append("svg")
.attr("viewBox", `0 0 ${config.width - 30} ${chartHeight}`)
.attr("width", "100%");
const chartWidth = config.width - 30 - chartMargin.left - chartMargin.right;
const chartInnerHeight = chartHeight - chartMargin.top - chartMargin.bottom;
const chartG = svg.append("g")
.attr("transform", `translate(${chartMargin.left}, ${chartMargin.top})`);
// Scales
const xScale = d3.scaleLinear()
.domain([0, 24])
.range([0, chartWidth]);
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([chartInnerHeight, 0]);
// Axes
chartG.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${chartInnerHeight})`)
.call(d3.axisBottom(xScale).ticks(12).tickFormat(d => `${d}:00`))
.selectAll("text")
.style("font-size", "10px")
.attr("transform", "rotate(-45)")
.attr("text-anchor", "end");
chartG.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(yScale).ticks(5).tickFormat(d => `${d} mW`))
.selectAll("text")
.style("font-size", "10px");
// Axis labels
svg.append("text")
.attr("x", chartMargin.left + chartWidth / 2)
.attr("y", chartHeight - 5)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "11px")
.text("Time of Day (hours)");
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -(chartMargin.top + chartInnerHeight / 2))
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "11px")
.text("Power Consumption");
// Grid lines
chartG.append("g")
.attr("class", "grid")
.selectAll("line")
.data(yScale.ticks(5))
.enter()
.append("line")
.attr("x1", 0)
.attr("x2", chartWidth)
.attr("y1", d => yScale(d))
.attr("y2", d => yScale(d))
.attr("stroke", config.colors.lightGray)
.attr("stroke-dasharray", "3,3");
// Area generators
const areaGenerator = d3.area()
.x(d => xScale(d.hour))
.y0(chartInnerHeight)
.y1(d => yScale(d.power))
.curve(d3.curveMonotoneX);
const lineGenerator = d3.line()
.x(d => xScale(d.hour))
.y(d => yScale(d.power))
.curve(d3.curveMonotoneX);
// Static profile area
const staticArea = chartG.append("path")
.attr("class", "static-area")
.attr("fill", config.colors.red)
.attr("fill-opacity", 0.2);
const staticLine = chartG.append("path")
.attr("class", "static-line")
.attr("fill", "none")
.attr("stroke", config.colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Adaptive profile area
const adaptiveArea = chartG.append("path")
.attr("class", "adaptive-area")
.attr("fill", config.colors.teal)
.attr("fill-opacity", 0.3);
const adaptiveLine = chartG.append("path")
.attr("class", "adaptive-line")
.attr("fill", "none")
.attr("stroke", config.colors.teal)
.attr("stroke-width", 3);
// Current time indicator
const timeIndicator = chartG.append("g")
.attr("class", "time-indicator");
timeIndicator.append("line")
.attr("class", "time-line")
.attr("y1", 0)
.attr("y2", chartInnerHeight)
.attr("stroke", config.colors.orange)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
timeIndicator.append("circle")
.attr("class", "time-dot")
.attr("r", 6)
.attr("fill", config.colors.orange);
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${chartMargin.left + 20}, 15)`);
legend.append("line")
.attr("x1", 0).attr("x2", 30)
.attr("y1", 0).attr("y2", 0)
.attr("stroke", config.colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
legend.append("text")
.attr("x", 35).attr("y", 4)
.attr("font-size", "11px")
.attr("fill", config.colors.red)
.text("Static Mode");
legend.append("line")
.attr("x1", 130).attr("x2", 160)
.attr("y1", 0).attr("y2", 0)
.attr("stroke", config.colors.teal)
.attr("stroke-width", 3);
legend.append("text")
.attr("x", 165).attr("y", 4)
.attr("font-size", "11px")
.attr("fill", config.colors.teal)
.text("Adaptive Mode");
// ---------------------------------------------------------------------------
// METRICS PANELS
// ---------------------------------------------------------------------------
const metricsContainer = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))")
.style("gap", "20px")
.style("margin-bottom", "20px");
// Current Power Display
const powerPanel = metricsContainer.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "15px")
.style("border", `2px solid ${config.colors.gray}`);
powerPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Current Power Breakdown");
const powerDisplay = powerPanel.append("div")
.attr("class", "power-display");
// Energy Savings Display
const savingsPanel = metricsContainer.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "15px")
.style("border", `2px solid ${config.colors.gray}`);
savingsPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Energy Savings Calculator");
const savingsDisplay = savingsPanel.append("div")
.attr("class", "savings-display");
// Battery Life Comparison
const batteryPanel = metricsContainer.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "15px")
.style("border", `2px solid ${config.colors.gray}`);
batteryPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Estimated Battery Life");
const batteryLifeDisplay = batteryPanel.append("div")
.attr("class", "battery-display");
// ---------------------------------------------------------------------------
// CALCULATION FUNCTIONS
// ---------------------------------------------------------------------------
function calculateStaticPower() {
// Static mode uses fixed settings regardless of context
const baseSamplingRate = 10; // Fixed 10 second sampling
const baseTxFrequency = 60; // Fixed 60 second transmission
const baseProcessing = "local";
// Calculate power components
const samplesPerHour = 3600 / baseSamplingRate;
const txPerHour = 3600 / baseTxFrequency;
// Sensor power (sampling time ~50ms per sample)
const sensorPower = (samplesPerHour * 0.05 * config.power.sensor.sampling) / 3600 +
config.power.sensor.idle * ((3600 - samplesPerHour * 0.05) / 3600);
// MCU power (active during sampling and processing)
const activeTime = samplesPerHour * 0.1; // 100ms per sample
const mcuPower = (activeTime * config.power.mcu.active) / 3600 +
config.power.mcu.idle * ((3600 - activeTime) / 3600);
// Radio power (transmission time ~100ms per tx, with retries)
const retryFactor = 1.0; // No adaptation for network quality
const txTime = txPerHour * 0.1 * retryFactor;
const radioPower = (txTime * config.power.radio.tx) / 3600 +
config.power.radio.idle * ((3600 - txTime) / 3600);
// Processing power
const processingPower = config.power.processing[baseProcessing] * (activeTime / 3600);
const totalPower = (sensorPower + mcuPower + radioPower + processingPower) * config.voltage;
return {
sensor: sensorPower * config.voltage,
mcu: mcuPower * config.voltage,
radio: radioPower * config.voltage,
processing: processingPower * config.voltage,
total: totalPower
};
}
function calculateAdaptivePower(hour) {
// Adaptive mode adjusts based on context
const timeContext = getTimeContext(hour !== undefined ? hour : state.timeOfDay);
const motion = hour !== undefined ? getExpectedMotion(hour) : state.motionDetected;
const battery = state.batteryLevel;
const network = state.networkQuality;
const urgency = state.dataUrgency;
// Adapt sampling rate based on context
let adaptedSamplingRate = state.samplingRate;
if (!motion) adaptedSamplingRate *= 3; // Less frequent when no motion
if (battery < 20) adaptedSamplingRate *= 2; // Less frequent on low battery
if (timeContext === "night") adaptedSamplingRate *= 2; // Less frequent at night
// Adapt transmission frequency
let adaptedTxFrequency = state.txFrequency;
if (network === "poor") adaptedTxFrequency *= 2; // Wait for better conditions
if (urgency === "low") adaptedTxFrequency *= 2; // Batch more data
if (urgency === "high") adaptedTxFrequency /= 2; // More frequent
if (battery < 20) adaptedTxFrequency *= 1.5; // Conserve battery
// Adapt processing location
let adaptedProcessing = state.processingLocation;
if (battery < 20 && adaptedProcessing === "local") {
adaptedProcessing = "edge"; // Offload when low battery
}
// Calculate power with adaptations
const samplesPerHour = 3600 / adaptedSamplingRate;
const txPerHour = 3600 / adaptedTxFrequency;
// Network retry factor
const retryFactor = network === "poor" ? 2.0 : network === "fair" ? 1.3 : 1.0;
// Sensor power
const sensorPower = motion ?
(samplesPerHour * 0.05 * config.power.sensor.sampling) / 3600 + config.power.sensor.idle * 0.9 :
config.power.sensor.idle; // Minimal when no motion
// MCU power with sleep optimization
const activeTime = motion ? samplesPerHour * 0.1 : samplesPerHour * 0.02;
const sleepFactor = timeContext === "night" ? 0.9 : 0.5;
const mcuPower = (activeTime * config.power.mcu.active) / 3600 +
config.power.mcu.sleep * sleepFactor +
config.power.mcu.idle * ((3600 - activeTime) / 3600) * (1 - sleepFactor);
// Radio power with adaptive transmission
const txTime = txPerHour * 0.1 * retryFactor;
const radioPower = (txTime * config.power.radio.tx) / 3600 +
config.power.radio.idle * ((3600 - txTime) / 3600);
// Processing power
const processingPower = config.power.processing[adaptedProcessing] * (activeTime / 3600);
const totalPower = (sensorPower + mcuPower + radioPower + processingPower) * config.voltage;
return {
sensor: sensorPower * config.voltage,
mcu: mcuPower * config.voltage,
radio: radioPower * config.voltage,
processing: processingPower * config.voltage,
total: totalPower,
adaptations: {
samplingRate: adaptedSamplingRate,
txFrequency: adaptedTxFrequency,
processing: adaptedProcessing
}
};
}
function getTimeContext(hour) {
if (hour >= 6 && hour < 9) return "morning";
if (hour >= 9 && hour < 17) return "work";
if (hour >= 17 && hour < 22) return "evening";
return "night";
}
function getExpectedMotion(hour) {
// Model typical activity patterns
if (hour >= 23 || hour < 6) return false; // Night
if (hour >= 6 && hour < 8) return true; // Morning routine
if (hour >= 8 && hour < 9) return false; // Commute
if (hour >= 9 && hour < 12) return true; // Work
if (hour >= 12 && hour < 13) return true; // Lunch
if (hour >= 13 && hour < 17) return true; // Work
if (hour >= 17 && hour < 18) return false; // Commute
if (hour >= 18 && hour < 22) return true; // Evening
return true;
}
function generate24HourProfile() {
const staticProfile = [];
const adaptiveProfile = [];
for (let hour = 0; hour <= 24; hour += 0.5) {
staticProfile.push({
hour: hour,
power: calculateStaticPower().total
});
adaptiveProfile.push({
hour: hour,
power: calculateAdaptivePower(hour).total
});
}
return { staticProfile, adaptiveProfile };
}
function updateCalculations() {
const staticPower = calculateStaticPower();
const adaptivePower = calculateAdaptivePower();
const profiles = generate24HourProfile();
// Update chart
const maxPower = Math.max(
d3.max(profiles.staticProfile, d => d.power),
d3.max(profiles.adaptiveProfile, d => d.power)
) * 1.1;
yScale.domain([0, maxPower]);
svg.select(".y-axis")
.transition()
.duration(300)
.call(d3.axisLeft(yScale).ticks(5).tickFormat(d => `${d.toFixed(0)} mW`));
staticArea.datum(profiles.staticProfile)
.transition()
.duration(300)
.attr("d", areaGenerator);
staticLine.datum(profiles.staticProfile)
.transition()
.duration(300)
.attr("d", lineGenerator);
adaptiveArea.datum(profiles.adaptiveProfile)
.transition()
.duration(300)
.attr("d", areaGenerator);
adaptiveLine.datum(profiles.adaptiveProfile)
.transition()
.duration(300)
.attr("d", lineGenerator);
// Update time indicator
const currentPower = state.adaptiveMode ? adaptivePower.total : staticPower.total;
timeIndicator.select(".time-line")
.attr("x1", xScale(state.timeOfDay))
.attr("x2", xScale(state.timeOfDay));
timeIndicator.select(".time-dot")
.attr("cx", xScale(state.timeOfDay))
.attr("cy", yScale(currentPower));
// Update power breakdown
const currentMode = state.adaptiveMode ? adaptivePower : staticPower;
powerDisplay.html(`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div style="background: white; padding: 8px; border-radius: 6px; text-align: center;">
<div style="color: ${config.colors.gray}; font-size: 10px;">Sensor</div>
<div style="color: ${config.colors.purple}; font-size: 16px; font-weight: bold;">${currentMode.sensor.toFixed(2)} mW</div>
</div>
<div style="background: white; padding: 8px; border-radius: 6px; text-align: center;">
<div style="color: ${config.colors.gray}; font-size: 10px;">MCU</div>
<div style="color: ${config.colors.navy}; font-size: 16px; font-weight: bold;">${currentMode.mcu.toFixed(2)} mW</div>
</div>
<div style="background: white; padding: 8px; border-radius: 6px; text-align: center;">
<div style="color: ${config.colors.gray}; font-size: 10px;">Radio</div>
<div style="color: ${config.colors.orange}; font-size: 16px; font-weight: bold;">${currentMode.radio.toFixed(2)} mW</div>
</div>
<div style="background: white; padding: 8px; border-radius: 6px; text-align: center;">
<div style="color: ${config.colors.gray}; font-size: 10px;">Processing</div>
<div style="color: ${config.colors.blue}; font-size: 16px; font-weight: bold;">${currentMode.processing.toFixed(2)} mW</div>
</div>
</div>
<div style="background: white; padding: 10px; border-radius: 6px; text-align: center; margin-top: 8px;">
<div style="color: ${config.colors.gray}; font-size: 10px;">Total Power</div>
<div style="color: ${state.adaptiveMode ? config.colors.teal : config.colors.red}; font-size: 24px; font-weight: bold;">
${currentMode.total.toFixed(2)} mW
</div>
</div>
${state.adaptiveMode && adaptivePower.adaptations ? `
<div style="margin-top: 8px; font-size: 11px; color: ${config.colors.gray};">
<strong>Active Adaptations:</strong> Sampling ${adaptivePower.adaptations.samplingRate.toFixed(0)}s,
TX ${adaptivePower.adaptations.txFrequency.toFixed(0)}s,
Processing: ${adaptivePower.adaptations.processing}
</div>
` : ''}
`);
// Calculate daily energy consumption
const staticDailyEnergy = profiles.staticProfile.reduce((sum, p, i, arr) =>
i > 0 ? sum + (p.power + arr[i-1].power) / 2 * 0.5 : sum, 0); // mWh
const adaptiveDailyEnergy = profiles.adaptiveProfile.reduce((sum, p, i, arr) =>
i > 0 ? sum + (p.power + arr[i-1].power) / 2 * 0.5 : sum, 0); // mWh
const savings = ((staticDailyEnergy - adaptiveDailyEnergy) / staticDailyEnergy * 100);
savingsDisplay.html(`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div style="background: white; padding: 10px; border-radius: 6px; text-align: center; border-left: 4px solid ${config.colors.red};">
<div style="color: ${config.colors.gray}; font-size: 10px;">Static Mode (24h)</div>
<div style="color: ${config.colors.red}; font-size: 18px; font-weight: bold;">${staticDailyEnergy.toFixed(1)} mWh</div>
</div>
<div style="background: white; padding: 10px; border-radius: 6px; text-align: center; border-left: 4px solid ${config.colors.teal};">
<div style="color: ${config.colors.gray}; font-size: 10px;">Adaptive Mode (24h)</div>
<div style="color: ${config.colors.teal}; font-size: 18px; font-weight: bold;">${adaptiveDailyEnergy.toFixed(1)} mWh</div>
</div>
</div>
<div style="background: ${config.colors.green}; padding: 12px; border-radius: 6px; text-align: center; margin-top: 10px; color: white;">
<div style="font-size: 12px;">Energy Savings</div>
<div style="font-size: 28px; font-weight: bold;">${savings.toFixed(1)}%</div>
<div style="font-size: 11px;">${(staticDailyEnergy - adaptiveDailyEnergy).toFixed(1)} mWh saved daily</div>
</div>
`);
// Calculate battery life
const batteryEnergy = config.batteryCapacity * config.voltage; // mWh
const staticBatteryLife = batteryEnergy / (staticDailyEnergy / 24); // hours
const adaptiveBatteryLife = batteryEnergy / (adaptiveDailyEnergy / 24); // hours
batteryLifeDisplay.html(`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div style="background: white; padding: 10px; border-radius: 6px;">
<div style="color: ${config.colors.gray}; font-size: 10px; text-align: center;">Static Mode</div>
<div style="color: ${config.colors.red}; font-size: 20px; font-weight: bold; text-align: center;">
${formatDuration(staticBatteryLife)}
</div>
<div style="height: 8px; background: ${config.colors.lightGray}; border-radius: 4px; margin-top: 5px; overflow: hidden;">
<div style="width: 40%; height: 100%; background: ${config.colors.red};"></div>
</div>
</div>
<div style="background: white; padding: 10px; border-radius: 6px;">
<div style="color: ${config.colors.gray}; font-size: 10px; text-align: center;">Adaptive Mode</div>
<div style="color: ${config.colors.teal}; font-size: 20px; font-weight: bold; text-align: center;">
${formatDuration(adaptiveBatteryLife)}
</div>
<div style="height: 8px; background: ${config.colors.lightGray}; border-radius: 4px; margin-top: 5px; overflow: hidden;">
<div style="width: ${Math.min(100, adaptiveBatteryLife / staticBatteryLife * 40)}%; height: 100%; background: ${config.colors.teal};"></div>
</div>
</div>
</div>
<div style="margin-top: 10px; text-align: center; color: ${config.colors.green}; font-size: 14px; font-weight: bold;">
+${((adaptiveBatteryLife / staticBatteryLife - 1) * 100).toFixed(0)}% longer battery life
</div>
`);
}
function formatDuration(hours) {
if (hours < 24) return `${hours.toFixed(1)} hrs`;
if (hours < 720) return `${(hours / 24).toFixed(1)} days`;
return `${(hours / 720).toFixed(1)} months`;
}
// ---------------------------------------------------------------------------
// INITIALIZE
// ---------------------------------------------------------------------------
updateCalculations();
return container.node();
}