This library provides a structured framework to simulate complex interventions and evolutions of entity stocks (e.g., building upgrades, urban planning, biodiversity restoration) under realistic constraints like budgets, spatial quotas, and timing.
It uses a generic Filter → Prioritise → Transform lifecycle that makes it immediately usable across domains beyond energy, including transport, conservation, and public health.
Every intervention is defined by three core functions (predicates) that run iteratively for each step (year, month, or custom event) of the simulation:
- Filter: Determines eligibility. It answers the question: "Which entities are candidates for this transformation right now?"
- Prioritise: Determines order. It answers the question: "In what order should we process the eligible entities?" (Critical when resources like budget or capacity are limited).
- Transform (Upgrade): Determines impact. It answers the question: "What change is applied, and what are the results?" This function performs the actual modification and returns metric deltas.
- Domain Agnostic: Works with any "Entity" (Buildings, Parcels, Habitat Patches, Fleet Corridors).
- Slim & Modular: Import only the core engine (~25KB) or add domain plugins manually to keep bundles small.
- Spatial First: Geometries are first-class properties. Carry GeoJSON through the simulation and use spatial predicates (
within,distanceTo). - Resource Layer: Global
resourcesAPI for cross-intervention constraints (e.g., "max 5 upgrades per km²", "global installer headroom", "regional budget envelopes"). - Flexible Timesteps: Configure the simulation loop by years, months, or custom event steps.
- Data Agnostic: Interface with any data source (CSV, JSON, GeoJSON, SQL) via the Facet Adapter pattern.
- Pure Client-Side: Run complex simulations (50k+ features) directly in the browser with no backend required.
npm install interactive-scenario-modellerThe library is designed to be as slim as possible. You can import just the core engine or include specific domain plugins manually to keep your production bundle small.
Best for general resource allocation, filtering, and "what-if" simulations.
import { SimulationRunner, Intervention } from 'interactive-scenario-modeller';Import only the logic you need for your target domain:
// Energy & Grid constraints
import { createSubstationCapacityGatePlugin } from 'interactive-scenario-modeller/plugins/grid';
// Financial & Budgeting
import { createBudgetSpendTrackerPlugin } from 'interactive-scenario-modeller/plugins/financial';
// Spatial & Geographic Prioritisation
import { createSpatialClusterPrioritiserPlugin } from 'interactive-scenario-modeller/plugins/geographic';
// Optimization (MCDA / Carbon Targets)
import { createMultiCriteriaPrioritiserPlugin } from 'interactive-scenario-modeller/plugins/optimization';If you need everything or are just prototyping:
import { installAllBundles } from 'interactive-scenario-modeller/plugins';import { Intervention, arrayAdapter, toGeoJSON } from 'interactive-scenario-modeller';
const entities = [
{ id: 'A', geometry: { type: 'Point', coordinates: [0, 0] }, value: 10 },
{ id: 'B', geometry: { type: 'Point', coordinates: [1, 1] }, value: 20 }
];
const intervention = new Intervention('Greening Project', {
facet: arrayAdapter(entities),
startYear: 2025,
endYear: 2030,
// Filter by spatial location or attributes
filter: (entity, context) => entity.value < 15,
// Ranks entities by potential
prioritise: (a, b) => b.value - a.value,
// Apply the transformation and consume global resources
transform: (entity, context) => {
if (context.resources.consume('budget', 1000)) {
return { greened: true, cost: 1000 };
}
}
});
const result = intervention.simulate(null, { budget: 50000 });
const geojson = toGeoJSON(intervention.simulatedFacet);The primary modelling primitive.
setupEntity/setupBuilding: Transform facet rows into entity objects.transform/apply/upgrade: Apply changes and return metrics.simulate(entities?, sharedResources?): Run the simulation loop.
Passed to every predicate, providing access to:
step/year: Current timestep.resources: Global resource API (get,set,consume,has).state: Local intervention state.random(): Deterministic RNG for reproducible scenarios.
toGeoJSON(facet): Export results to a GeoJSON FeatureCollection if geometries are present.registerSpatialPredicates(): Addsgeo:distanceToandgeo:withinto the registry.
- The Facet Adapter Pattern: Decouples logic from data engineering. Wrap any source in
FacetLike. - Predicate-Based Control: Express complex, real-world logic directly in code rather than rigid config files.
- Minimal Core with Extensibility: Specialized logic (grid, finance, risk) is handled through a plugin system.
- Transparency: Break down "black box" simulations into auditable, testable steps.
Use this as a quick checklist when preparing your dataset. All field names are typically configurable in plugin options, but these are the common defaults:
| Plugin family | Typical required fields | Typical optional fields |
|---|---|---|
| Financial | id, intervention cost field (for example estimatedPVCost) |
capex, opex, financing parameters, discount-rate inputs |
| Social | id, fuelPovertyScore (or equivalent priority metric) |
deprivationIndex, vulnerability flags, tenure |
| Policy | id, year/timeline fields used by policy rules |
local policy zone, planning class, permit status |
| Grid | id, substationId, demand/export fields used by checks |
feeder id, constrained-area flag, upgrade queue status |
| Optimization | id, benefit + cost fields (for ranking/constraints) |
readiness score, confidence/uncertainty score |
| Geographic | id, geographic grouping field (region, lsoa, etc.) |
urban/rural class, spatial cluster id, district code |
| Timeseries | id, seasonal/load profile inputs |
flexibility score, hourly shape id, storage coupling fields |
| Transport | id, EV charging demand/load fields, corridor/group id |
charger type mix, fleet assumptions, travel-demand class |
| Risk | id, baseline metric(s) to perturb (cost, demand, carbon, etc.) |
scenario tags, volatility class, sensitivity labels |
When in doubt, start with id + the exact fields referenced by your filter, prioritise, and upgrade functions. If you are using energy-specific plugins, they may default to looking for uprn but this can be mapped to any id field in your configuration.
A facet is the input data interface consumed by Intervention. It is intentionally minimal and can wrap arrays, tables, or external data structures.
Required shape:
type FacetLike = {
getRowCount?: () => number;
getRow?: (i: number) => any;
colNames?: string[];
};arrayAdapter() converts a plain Array<object> into this interface:
const facet = arrayAdapter(buildings);
const intervention = new Intervention('My intervention', { facet });If you already have your own table abstraction, just provide getRowCount() and getRow(i) and pass it directly as facet.
intervention.simulate() returns:
state— mutable scenario state accumulated viainit,upgrade, and year hooksmetrics— per-year upgrade events and metric deltasbuildings— final in-memory building objects after simulationcolumns—Set<string>of declared output columns frompropertySpec()
Example shape:
const result = intervention.simulate();
// result.state
// { budgetSpent: { 2024: 1200000, 2025: 1800000 } }
// result.metrics
// {
// "2024": [
// {
// building: "100001",
// stats: {
// installed: true,
// cost: 5000,
// building: "100001",
// year: 2024,
// order: 1
// }
// }
// ]
// }
// result.buildings
// [ { uprn: "100001", ...updated fields... } ]
// result.columns
// Set { "cost", "installed", ... }The intervention also stores intervention.simulatedFacet, a facet-like tabular output built from metrics (with columns like uprn, year, metric fields, plus optional carried-through input columns).
Use prebuilt predicates/plugins for common scenario-modeller flows:
import {
Intervention,
arrayAdapter,
installScenarioModellerPresets,
} from 'interactive-scenario-modeller';
const { predicates, pluginExports } = installScenarioModellerPresets({
namespace: 'scenario',
defaultMinScoreThreshold: 0.6,
});
const facet = arrayAdapter([{ uprn: '1', estimatedPVCost: 5000 }]);
const intervention = new Intervention('PV rollout', {
facet,
startYear: 2024,
endYear: 2030,
filter: predicates.budgetConstraint,
// or use plugin export syntax: filter: pluginExports.planningConstraint,
upgrade: (building, context) => {
const year = context.year;
const cost = building.estimatedPVCost ?? 0;
context.state.budgetSpent = context.state.budgetSpent ?? {};
context.state.budgetSpent[year] = (context.state.budgetSpent[year] ?? 0) + cost;
return { installed: true, cost };
},
});
const result = intervention.simulate();Preset predicates included:
budgetConstraintplanningConstraintphasedRolloutmultiObjectivePrioritization(Filtering threshold)multiObjectivePrioritiser(Ranking order)policyEvolution
Plugin export references included:
pluginExports.budgetConstraintpluginExports.planningConstraintpluginExports.phasedRolloutpluginExports.multiObjectivePrioritization(Filtering threshold)pluginExports.multiObjectivePrioritiser(Ranking order)pluginExports.policyEvolution
createBudgetSpendTrackerPlugin(opts)— reusableupgradeplugin for yearly budget spend trackingcreateFinancingModelPlugin(opts)— reusableupgradeplugin for cash/lease/PPA outputs (annualPayment,npvCost,modelType)
createFuelPovertyPriorityPlugin(opts)— reusableconstraintplugin using weighted fuel-poverty/carbon priority scoring
createPolicyTimelineValidatorPlugin(opts)— reusableconstraintplugin for timeline schema validation and optional strict year coverage
createSubstationCapacityGatePlugin(opts)— reusableconstraintplugin to gate upgrades by per-substation capacitycreateSequentialEnablementPlugin(opts)— reusableconstraintplugin for prerequisite intervention gatingcreateGenerationHeadroomAllocationPlugin(opts)— reusableconstraintplugin for allocating substation export headroom to utility-scale renewablescreateGridEnergyBalanceReportingPlugin(opts)— reusableupgradeplugin to calculate demand+generation requirements and remaining headroom
createCostBenefitPrioritiserPlugin(opts)— reusableprioritiserplugin for benefit-per-cost orderingcreateMultiCriteriaPrioritiserPlugin(opts)— general-purposeprioritiserplugin using weighted sum model with functional getters (ideal for AHP)createCarbonTargetConstraintPlugin(opts)— reusableconstraintplugin to stop selection once target is reachedcreateTopPercentPotentialPlugin(opts)— reusableconstraintplugin for pre-ranked top-percent targeting
createSpatialClusterPrioritiserPlugin(opts)— reusableprioritiserplugin for cluster-density strategycreateUrbanRuralStrategyPlugin(opts)— reusable combinedconstraint+prioritiserplugin for area strategycreateRegionBudgetSplitPlugin(opts)— reusableconstraintplugin for per-region budget envelopes
createSeasonalDemandGatePlugin(opts)— reusableconstraintplugin for seasonal demand/capacity gatingcreateLoadProfileScoringPlugin(opts)— reusableprioritiserplugin for peak/load-shift/flexibility scoringcreateTechnologyCouplingPlugin(opts)— reusableupgradeplugin for battery/heat-pump adjusted demand and carbon effects
createEVLoadInteractionPlugin(opts)— reusableconstraintplugin that combines baseline EV load and per-building EV charging demand in capacity checkscreateTransportCorridorConstraintPlugin(opts)— reusableconstraintplugin for corridor-level charging delivery requirements
createVolatilityScenarioPlugin(opts)— reusableupgradeplugin that applies year/season volatility multipliers to price metrics
installFinancialPlugins(opts)installSocialPlugins(opts)installPolicyPlugins(opts)installGridPlugins(opts)installOptimizationPlugins(opts)installGeographicPlugins(opts)installTimeseriesPlugins(opts)installTransportPlugins(opts)installRiskPlugins(opts)installAllPlugins(opts)
import { installAllPlugins } from 'interactive-scenario-modeller';
const plugins = installAllPlugins();
// Example: plugins.grid.substationCapacityGate.exportRefcreateScenarioTemplate(opts)— combines bundle installation and reusable state-default init hooks
import { createScenarioTemplate } from 'interactive-scenario-modeller';
const template = createScenarioTemplate();
// Example: template.init or template.withInit(...)See the end-to-end example in examples/scenario-template.ts. For staged big-data runs, see examples/large-dataset-workflow.ts. For grid export allocation and demand/headroom balance with mock data, see examples/grid-headroom-balance-workflow.ts. For a three-stage integrated flow (generation allocation, demand-side upgrades, then EV transport interaction), see examples/integrated-grid-two-stage-workflow.ts.
runSensitivityAnalysis(opts)— runs cartesian parameter sweeps with per-scenario summariesrunMonteCarloAnalysis(opts)— runs repeated stochastic scenarios with aggregate stats
- Avoid
eval()by default. Use predicate registry for named functions. - Keep interfaces small and testable. The library is TypeScript-first.