npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anephenix/systems

v0.0.18

Published

Systems

Readme

Systems

A Node.js library for the logic used in Systems Thinking diagrams (also known as Causal Loop Diagrams).

npm version Node.js CI Socket Badge

Install

npm i @anephenix/systems

Usage

To begin with, you need to create a System that can contain all of the entities, relations and loops that exist within it. For example, you could map out an ecological system like the populations of a predator and prey pair.

System

import { System } from '@anephenix/systems';

const system = new System({ name: 'Ecological system' });

Once you have created a system, you can begin to add entities to it.

Entities

Entities are things that you want to measure or that have an impact within the system. A good example in the context of the ecological system is the number of predators and the number of prey. In the case of Yellowstone national park in the United States, wolves are a predator and elk are one of their prey.

import { Entity } from '@anephenix/systems';

const wolves = new Entity({ name: 'Number of Wolves', type: 'quantifiable' });
const elk = new Entity({ name: 'Number of Elk', type: 'quantifiable' });

The type property indicates whether the entity can be measured numerically. We can count wolves, so the type is 'quantifiable'.

The other type is 'non-quantifiable', for things that cannot be measured numerically — for example, emotional state or public confidence.

Relations

Relations are the directed links between entities. Each relation goes from one entity to another and has either a positive or negative polarity.

Negative relation — an increase in the source causes a decrease in the target (and vice versa). Wolves preying on elk is a negative link: more wolves means fewer elk.

import { Entity, Relation } from '@anephenix/systems';

const wolves = new Entity({ name: 'Number of Wolves', type: 'quantifiable' });
const elk = new Entity({ name: 'Number of Elk', type: 'quantifiable' });

const wolvesEatElk = new Relation({
    name: 'Wolves eating Elk',
    type: 'negative',
    from: wolves.id,
    to: elk.id,
});

Positive relation — an increase in the source causes an increase in the target. Elk as a food source for wolves is a positive link: more elk eventually means more wolves.

const elkAsFoodForWolves = new Relation({
    name: 'Elk as a food source for Wolves',
    type: 'positive',
    from: elk.id,
    to: wolves.id,
});

Adding entities and relations to the system

Use addEntity and addRelation on the system instance to register everything.

import { System, Entity, Relation } from '@anephenix/systems';

const system = new System({ name: 'Ecological system' });

const wolves = new Entity({ name: 'Number of Wolves', type: 'quantifiable' });
const elk = new Entity({ name: 'Number of Elk', type: 'quantifiable' });

const wolvesEatElk = new Relation({
    name: 'Wolves eating Elk',
    type: 'negative',
    from: wolves.id,
    to: elk.id,
});

const elkAsFoodForWolves = new Relation({
    name: 'Elk as a food source for Wolves',
    type: 'positive',
    from: elk.id,
    to: wolves.id,
});

system.addEntity(wolves);
system.addEntity(elk);

system.addRelation(wolvesEatElk);
system.addRelation(elkAsFoodForWolves);

Detecting feedback loops

Feedback loops are cyclical patterns that emerge from the relationships between entities. Once all entities and relations have been added, call detectLoops() to find and classify every loop automatically.

system.detectLoops();
console.log(system.loops);

Each loop in system.loops has an id, a type ('balancing' or 'reinforcing'), and arrays of the entity and relation IDs that make it up.

Loops are re-detected automatically whenever you update or remove an entity or relation.

Balancing loops

A balancing loop is self-regulating — entities oscillate around an equilibrium. The wolves-and-elk system is a classic example: more wolves reduces elk, which eventually reduces wolves, which allows elk to recover, and so on.

Reinforcing loops

A reinforcing loop amplifies change in one direction. A savings account is a good example: a higher balance earns more interest, which further increases the balance.

Simulations

A Simulation takes a system with assigned initial values and runs it forward step by step, recording the value of every entity at each step.

Basic setup
import { System, Entity, Relation, Simulation } from '@anephenix/systems';

const system = new System({ name: 'Ecological system' });

const wolves = new Entity({ name: 'Number of Wolves', type: 'quantifiable' });
const elk = new Entity({ name: 'Number of Elk', type: 'quantifiable' });

system.addEntity(wolves);
system.addEntity(elk);

const wolvesEatElk = new Relation({
    name: 'Wolves eating Elk',
    type: 'negative',
    from: wolves.id,
    to: elk.id,
});

const elkAsFoodForWolves = new Relation({
    name: 'Elk as a food source for Wolves',
    type: 'positive',
    from: elk.id,
    to: wolves.id,
});

system.addRelation(wolvesEatElk);
system.addRelation(elkAsFoodForWolves);

const simulation = new Simulation({
    system,
    initialValues: {
        [wolves.id]: 10,
        [elk.id]: 200,
    },
    steps: 50,  // number of time steps to run
    dt: 1,      // time delta per step (e.g. 1 = one season)
});
Running the simulation

Call run() to execute all steps at once. It returns the full history — an array of { step, time, values } objects, one per step starting at step 0 (the initial state).

const history = simulation.run();

for (const step of history) {
    console.log(step.step, step.time, step.values);
}

At each step, every entity's new value is computed from the previous step's values using the formula:

new_value = old_value + (sum of incoming relation deltas) × dt

For a positive relation the delta adds to the target; for a negative relation it subtracts. All entities update simultaneously, so no ordering bias is introduced.

Relation weights

By default each relation has a weight of 1.0. You can override this per relation to control how strongly one entity influences another.

const simulation = new Simulation({
    system,
    initialValues: {
        [wolves.id]: 10,
        [elk.id]: 200,
    },
    relationWeights: {
        [wolvesEatElk.id]: 0.05,       // each wolf removes 0.05 elk per step
        [elkAsFoodForWolves.id]: 0.008, // each elk adds 0.008 wolves per step
    },
    steps: 50,
});
Transfer functions

For relationships that are multiplicative rather than linear — for example, compound interest, or predator-prey dynamics where the interaction rate scales with both populations — you can supply a transfer function for any relation. The function receives (sourceValue, targetValue) and returns the delta magnitude; the relation's polarity is still applied on top.

import { System, Entity, Relation, Simulation } from '@anephenix/systems';

const system = new System({ name: 'Bank account' });

const interestRate = new Entity({ name: 'Annual interest rate', type: 'quantifiable' });
const balance = new Entity({ name: 'Balance', type: 'quantifiable' });

system.addEntity(interestRate);
system.addEntity(balance);

const rateGrowsBalance = new Relation({
    name: 'Interest rate compounds balance',
    type: 'positive',
    from: interestRate.id,
    to: balance.id,
});

system.addRelation(rateGrowsBalance);

const simulation = new Simulation({
    system,
    initialValues: {
        [interestRate.id]: 0.0325, // 3.25% AER
        [balance.id]: 1000,
    },
    relationTransferFns: {
        // delta = rate × currentBalance gives exact compound growth
        [rateGrowsBalance.id]: (rate, currentBalance) => rate * currentBalance,
    },
    steps: 10,
    dt: 1, // 1 year per step
});

const history = simulation.run();
// balance after 10 years: £1,376.89 (= 1000 × 1.0325^10)

When a transfer function is provided for a relation, it takes precedence over any relationWeights entry for that same relation.

Stepping manually

Instead of run(), you can advance the simulation one step at a time using advance(). This is useful when you want to inspect or react to values between steps.

const step1 = simulation.advance(); // returns { step: 1, time: 1, values: { ... } }
const step2 = simulation.advance(); // returns { step: 2, time: 2, values: { ... } }

If advance() is called before run(), the initial state (step 0) is recorded automatically.

Accessing history

getValuesAt(n) returns the entity values at a specific step number.

simulation.run();

const initialState = simulation.getValuesAt(0);
const afterFiveSteps = simulation.getValuesAt(5);
Resetting

reset() clears the history so the simulation can be run again from the initial values, for example with different parameters.

simulation.reset();

Licence and Credits

©2026; Anephenix Ltd.