diff --git a/modules.json b/modules.json index 34bd2e4f5..914bd7bc8 100644 --- a/modules.json +++ b/modules.json @@ -111,5 +111,10 @@ }, "communication": { "tabs": [] + }, + "nbody": { + "tabs": [ + "Nbody" + ] } } \ No newline at end of file diff --git a/package.json b/package.json index 92977dd97..f90bf530c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@types/plotly.js-dist": "npm:@types/plotly.js", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", - "@types/three": "^0.161.2", + "@types/three": "^0.163.0", "@vitejs/plugin-react": "^4.0.4", "acorn": "^8.8.1", "acorn-jsx": "^5.3.2", @@ -109,6 +109,7 @@ "js-slang": "^1.0.55", "lodash": "^4.17.21", "mqtt": "^4.3.7", + "nbody": "^0.1.1", "os": "^0.1.2", "patch-package": "^6.5.1", "phaser": "^3.54.0", @@ -122,7 +123,7 @@ "save-file": "^2.3.1", "source-academy-utils": "^1.0.0", "source-academy-wabt": "^1.0.4", - "three": "^0.162.0", + "three": "^0.163.0", "tslib": "^2.3.1", "uniqid": "^5.4.0", "url": "^0.11.3" diff --git a/src/bundles/nbody/CelestialBody.ts b/src/bundles/nbody/CelestialBody.ts new file mode 100644 index 000000000..b8e14fbd9 --- /dev/null +++ b/src/bundles/nbody/CelestialBody.ts @@ -0,0 +1,75 @@ +import { CelestialBody, type Vector3 } from 'nbody'; + +/** + * Create a new celestial body. + * @param label label of the body. + * @param mass mass of the body. + * @param position position of the body. + * @param velocity velocity of the body. + * @param acceleration acceleration of the body. + * @returns A new celestial body. + * @category Celestial Body + */ +export function createCelestialBody(label: string, mass: number, radius?: number, position?: Vector3, velocity?: Vector3, acceleration?: Vector3): CelestialBody { + return new CelestialBody(label, mass, radius, position, velocity, acceleration); +} + +/** + * Get the label of a celestial body. + * @param body The celestial body. + * @returns The label of the celestial body. + * @category Celestial Body + */ +export function getLabel(body: CelestialBody): string { + return body.label; +} + +/** + * Get the mass of a celestial body. + * @param body The celestial body. + * @returns The mass of the celestial body. + * @category Celestial Body + */ +export function getMass(body: CelestialBody): number { + return body.mass; +} + +/** + * Get the radius of a celestial body. + * @param body The celestial body. + * @returns The radius of the celestial body. + * @category Celestial Body + */ +export function getRadius(body: CelestialBody): number { + return body.radius; +} + +/** + * Get the position of a celestial body. + * @param body The celestial body. + * @returns The position of the celestial body. + * @category Celestial Body + */ +export function getPosition(body: CelestialBody): Vector3 { + return body.position; +} + +/** + * Get the velocity of a celestial body. + * @param body The celestial body. + * @returns The velocity of the celestial body. + * @category Celestial Body + */ +export function getVelocity(body: CelestialBody): Vector3 { + return body.velocity; +} + +/** + * Get the acceleration of a celestial body. + * @param body The celestial body. + * @returns The acceleration of the celestial body. + * @category Celestial Body + */ +export function getAcceleration(body: CelestialBody): Vector3 { + return body.acceleration; +} diff --git a/src/bundles/nbody/Force.ts b/src/bundles/nbody/Force.ts new file mode 100644 index 000000000..ab7ffa3b4 --- /dev/null +++ b/src/bundles/nbody/Force.ts @@ -0,0 +1,41 @@ +import { CentripetalForce, CombinedForce, Gravity, type CelestialBody, type Force, type Vector3, LambdaForce } from 'nbody'; + +/** + * Create a force that applies to all bodies using the provided higher order/lambda/arrow/anonymous function. + * @param fn A function that takes an array of bodies and returns an array of forces of the same length. + * @returns A new lambda force. + * @category Forces + */ +export function createForce(fn: (bodies: CelestialBody[]) => Vector3[]): Force { + return new LambdaForce(fn); +} + +/** + * Create a force that applies to all bodies. + * @param G The gravitational constant. + * @returns A new gravity force. + * @category Forces + */ +export function createGravity(G?: number): Gravity { + return new Gravity(G); +} + +/** + * Create a centripetal force that pulls bodies towards a center. + * @param center The center of the centripetal force. + * @returns A new centripetal force. + * @category Forces + */ +export function createCentripetalForce(center?: Vector3): CentripetalForce { + return new CentripetalForce(center); +} + +/** + * Create a combined force that is an additive combination of all the given forces. + * @param forces The forces to combine. + * @returns A new combined force. + * @category Forces + */ +export function createCombinedForce(forces: Force[]): CombinedForce { + return new CombinedForce(forces); +} diff --git a/src/bundles/nbody/Misc.ts b/src/bundles/nbody/Misc.ts new file mode 100644 index 000000000..2020c2497 --- /dev/null +++ b/src/bundles/nbody/Misc.ts @@ -0,0 +1,14 @@ +import type { CelestialBody, State, Universe, Vector3 } from 'nbody'; + +/** + * Deep clone an object. + * @param obj The object to clone. + * @returns The cloned object. + * @category Celestial Body + * @category State + * @category Universe + * @category Vector + */ +export function clone(obj: CelestialBody | State | Universe | Vector3): CelestialBody | State | Universe | Vector3 { + return obj.clone(); +} diff --git a/src/bundles/nbody/SimulateFunction.ts b/src/bundles/nbody/SimulateFunction.ts new file mode 100644 index 000000000..bb64a354c --- /dev/null +++ b/src/bundles/nbody/SimulateFunction.ts @@ -0,0 +1,52 @@ +import { ExplicitEulerSim, LambdaSim, RungeKutta4Sim, SemiImplicitEulerSim, VelocityVerletSim, type Force, type State } from 'nbody'; + +/** + * Create an explicit euler integrator to be used as the simulation function. + * @param force The force that applies to the nbody system. + * @returns A new explicit Euler simulation. + * @category Simulate Functions + */ +export function createExplicitEulerSim(force?: Force): ExplicitEulerSim { + return new ExplicitEulerSim(force); +} + +/** + * Create a numerical integrator that uses the Runge-Kutta 4 method to simulate the nbody system. + * @param force The force that applies to the nbody system. + * @param weights The weights to be used for the weighted sum of the k values. + * @returns A new Runge-Kutta 4 simulation. + * @category Simulate Functions + */ +export function createRungeKutta4Sim(force?: Force, weights?: number[]): RungeKutta4Sim { + return new RungeKutta4Sim(force, weights); +} + +/** + * Create a numerical integrator that uses the semi-implicit Euler method to simulate the nbody system. + * @param force The force that applies to the nbody system. + * @returns A new semi-implicit Euler simulation. + * @category Simulate Functions + */ +export function createSemiImplicitEulerSim(force?: Force): SemiImplicitEulerSim { + return new SemiImplicitEulerSim(force); +} + +/** + * Create a numerical integrator that uses the velocity Verlet method to simulate the nbody system. + * @param force The force that applies to the nbody system. + * @returns A new velocity Verlet simulation. + * @category Simulate Functions + */ +export function createVelocityVerletSim(force: Force): VelocityVerletSim { + return new VelocityVerletSim(force); +} + +/** + * Create a simulate function (usually a numerical integrator) that is used to simulate the nbody system using the provided higher order/lambda/arrow/anonymous function. + * @param fn The function to be used as the simulate function. + * @returns A new lambda simulation. + * @category Simulate Functions + */ +export function createLambdaSim(fn: (deltaT: number, prevState: State, currState: State) => State): LambdaSim { + return new LambdaSim(fn); +} diff --git a/src/bundles/nbody/Simulation.ts b/src/bundles/nbody/Simulation.ts new file mode 100644 index 000000000..ad40b2b1b --- /dev/null +++ b/src/bundles/nbody/Simulation.ts @@ -0,0 +1,86 @@ +import context from 'js-slang/context'; +import { RecordingVisualizer, RecordingVisualizer3D, Simulation, type Universe, type VisType } from 'nbody'; + +/** + * Create a new simulation. + * @param universes The universes to simulate. + * @param visType The visualization type. + * @param record Whether to record the simulation. + * @param looped Whether to loop the simulation. + * @param showTrails Whether to show trails. + * @param showDebugInfo Whether to show debug info + * @param maxTrailLength The maximum length of trails. + * @returns A new simulation. + * @category Simulation + */ +export function createSimulation(universes: Universe[], + visType: VisType, + record?: boolean, + looped?: boolean, + showTrails?: boolean, + maxTrailLength?: number): Simulation { + return new Simulation(universes, { + visType, + record, + looped, + controller: 'code', + showTrails, + maxTrailLength, + }); +} + +const simulations: Simulation[] = []; +const recordInfo = { + isRecording: false, + recordFor: 0, + recordSpeed: 0, +}; + +context.moduleContexts.nbody.state = { + simulations, + recordInfo +}; + +function isRecordingBased(sim: Simulation): boolean { + return sim.visualizer instanceof RecordingVisualizer || sim.visualizer instanceof RecordingVisualizer3D; +} + +/** + * Play a simulation. + * @param sim The simulation to play. + * @category Simulation + */ +export function playSim(sim: Simulation): void { + while (simulations.length > 0) { + simulations.pop()!.stop(); + } + if (isRecordingBased(sim)) { + throw new Error( + 'playSim expects non-recording simulations' + ); + } + recordInfo.isRecording = false; + simulations.push(sim); +} + +/** + * Record and play a simulation. + * @param sim simulation to record and play. + * @param recordFor time to record for. + * @param recordSpeed speed to record at. + * @category Simulation + */ +export function recordSim(sim: Simulation, recordFor: number, recordSpeed: number): void { + while (simulations.length > 0) { + simulations.pop()!.stop(); + } + if (!isRecordingBased(sim)) { + throw new Error( + 'recordSim expects recording simulations' + ); + } + recordInfo.isRecording = true; + recordInfo.recordFor = recordFor; + recordInfo.recordSpeed = recordSpeed; + simulations.push(sim); +} diff --git a/src/bundles/nbody/State.ts b/src/bundles/nbody/State.ts new file mode 100644 index 000000000..66ac9f461 --- /dev/null +++ b/src/bundles/nbody/State.ts @@ -0,0 +1,21 @@ +import { type CelestialBody, State } from 'nbody'; + +/** + * Create a new state snapshot of the universe. + * @param bodies The bodies in the state. + * @returns A new state. + * @category State + */ +export function createState(bodies: CelestialBody[]): State { + return new State(bodies); +} + +/** + * Get the bodies in a state. + * @param state The state. + * @returns The bodies in the state. + * @category State + */ +export function getBodies(state: State): CelestialBody[] { + return state.bodies; +} diff --git a/src/bundles/nbody/Transformation.ts b/src/bundles/nbody/Transformation.ts new file mode 100644 index 000000000..f54fe438c --- /dev/null +++ b/src/bundles/nbody/Transformation.ts @@ -0,0 +1,74 @@ +import { BodyCenterTransformation, CoMTransformation, RotateTransformation, LambdaTransformation, type Vector3, type State, type Transformation, PinTransformation, TimedRotateTransformation } from 'nbody'; + +/** + * Create a frame of reference transformation that moves the origin to the center of ith both. + * @param i The index of the body to center on. + * @returns A new body center transformation. + * @category Transformations + */ +export function createBodyCenterTransformation(i?: number): BodyCenterTransformation { + return new BodyCenterTransformation(i); +} + +/** + * Create a frame of reference transformation that moves the origin to the center of mass of the system. + * @returns A new center of mass transformation. + * @category Transformations + */ +export function createCoMTransformation(): CoMTransformation { + return new CoMTransformation(); +} + +/** + * Create a frame of reference transformation that rotates the system around an axis by an angle. + * @param axis The axis to rotate around. + * @param angle The angle to rotate by. + * @returns A new rotate transformation. + * @category Transformations + */ +export function createRotateTransformation(axis: Vector3, angle: number): RotateTransformation { + return new RotateTransformation(axis, angle); +} + +/** + * Create a frame of reference transformation using a higher order/lambda/arrow/anonymous function. + * @param fn A function that takes a state and returns a new transformed state. + * @returns A new lambda transformation. + * @category Transformations + */ +export function createLambdaTransformation(fn: (state: State) => State): LambdaTransformation { + return new LambdaTransformation(fn); +} + +/** + * Create a frame of reference transformation that pins the ith body to a specific axis. + * @param axis The axis to pin the body to. + * @param i The index of the body to pin. + * @returns A new pin transformation. + * @category Transformations + */ +export function createPinTransformation(axis: Vector3, i?: number): PinTransformation { + return new PinTransformation(axis, i); +} + +/** + * Create a frame of reference transformation that rotates the system by 360 deg around an axis over a period of time. + * @param axis axis to rotate around. + * @param revolutionTime time taken to complete one revolution. + * @returns A new timed rotate transformation. + * @category Transformations + */ +export function createTimedRotateTransformation(axis: Vector3, revolutionTime: number): TimedRotateTransformation { + return new TimedRotateTransformation(axis, revolutionTime); +} + +/** + * Transform a state using a transformation. + * @param s The state to transform. + * @param t The transformation to apply. + * @returns The transformed state. + * @category Transformations + */ +export function transformState(s: State, t: Transformation): State { + return t.transform(s, 0); +} diff --git a/src/bundles/nbody/Universe.ts b/src/bundles/nbody/Universe.ts new file mode 100644 index 000000000..1f228a73a --- /dev/null +++ b/src/bundles/nbody/Universe.ts @@ -0,0 +1,30 @@ +import { type State, Universe, type SimulateFunction, type Transformation } from 'nbody'; + +/** + * Create a new universe. + * @param label The label of the universe. + * @param color The color of the universe. Can be a string or an array of strings of the same length as the number of bodies in the universe. + * @param prevState The previous state of the universe. + * @param currState The current state of the universe. + * @param simFunc The simulation function of the universe. + * @param transformations The transformations of the universe. + * @returns A new universe. + * @category Universe + */ +export function createUniverse( + label: string, + color: string[] | string, + prevState: State | undefined, + currState: State, + simFunc: SimulateFunction, + transformations: Transformation[], +): Universe { + return new Universe({ + label, + color, + prevState, + currState, + simFunc, + transformations, + }); +} diff --git a/src/bundles/nbody/Vector.ts b/src/bundles/nbody/Vector.ts new file mode 100644 index 000000000..1c68dd3a7 --- /dev/null +++ b/src/bundles/nbody/Vector.ts @@ -0,0 +1,109 @@ +import { Vector3 } from 'nbody'; + +/** + * Create a new 3D vector. + * @param x x component of the vector. + * @param y y component of the vector. + * @param z z component of the vector. + * @returns A new 3D vector. + * @category Vector + */ +export function createVector(x: number, y: number, z: number): Vector3 { + return new Vector3(x, y, z); +} + +/** + * Get the x component of a vector. + * @param v The vector. + * @returns The x component of the vector. + * @category Vector + */ +export function getX(v: Vector3): number { + return v.x; +} + +/** + * Get the y component of a vector. + * @param v The vector. + * @returns The y component of the vector. + * @category Vector + */ +export function getY(v: Vector3): number { + return v.y; +} + +/** + * Get the z component of a vector. + * @param v The vector. + * @returns The z component of the vector. + * @category Vector + */ +export function getZ(v: Vector3): number { + return v.z; +} + +/** + * Set the x component of a vector. + * @param v The vector. + * @param x The new x component. + * @category Vector + */ +export function setX(v: Vector3, x: number): void { + v.x = x; +} + +/** + * Set the y component of a vector. + * @param v The vector. + * @param y The new y component. + * @category Vector + */ +export function setY(v: Vector3, y: number): void { + v.y = y; +} + +/** + * Set the z component of a vector. + * @param v The vector. + * @param z The new z component. + * @category Vector + */ +export function setZ(v: Vector3, z: number): void { + v.z = z; +} + +/** + * Add two vectors. + * @param v1 The first vector. + * @param v2 The second vector. + * @returns The sum of the two vectors. + * @category Vector + */ +export function addVectors(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3() + .addVectors(v1, v2); +} + +/** + * Subtract two vectors. + * @param v1 The first vector. + * @param v2 The second vector. + * @returns The difference of the two vectors. + * @category Vector + */ +export function subVectors(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3() + .subVectors(v1, v2); +} + +/** + * Multiply a vector by a scalar. + * @param v vector to multiply. + * @param s scalar to multiply by. + * @returns The vector multiplied by the scalar. + * @category Vector + */ +export function multiplyScalar(v: Vector3, s: number): Vector3 { + return new Vector3() + .multiplyScalar(s); +} diff --git a/src/bundles/nbody/index.ts b/src/bundles/nbody/index.ts new file mode 100644 index 000000000..ca2b23c57 --- /dev/null +++ b/src/bundles/nbody/index.ts @@ -0,0 +1,74 @@ +/** + * Define, configure, simulate and visualize nbody systems. + * + * n-body systems are notoriuosly difficult to predict due to the chaotic nature of the interactions between the bodies. While some special cases can be solved analytically, most systems require precise and complex simulations and predict their behavior. This library provides a simple interface to define, configure, simulate and visualize nbody systems using a variety of predefined numerical integrators and visualization modes. + * + * ``` + * import { createCelestialBody, createGravity, createRungeKutta4Sim, + * createVelocityVerletSim, createSimulation, playSim, createState, createUniverse, + * createVector, clone, recordSim } from 'nbody'; + * + * const force = createGravity(1); + * const sim = createRungeKutta4Sim(force, [1, 2, 2, 1]); + * const a = createCelestialBody( + * "a", + * 1, + * 1, + * createVector(-0.97000436, 0.24308753, 0), + * createVector(0.466203685, 0.43236573, 0), + * createVector(0, 0, 0) + * ); + * const b = createCelestialBody( + * "b", + * 1, + * 1, + * createVector(0.97000436, -0.24308753, 0), + * createVector(0.466203685, 0.43236573, 0), + * createVector(0, 0, 0) + * ); + * const c = createCelestialBody( + * "c", + * 1, + * 1, + * createVector(0, 0, 0), + * createVector(-2 * 0.466203685, -2 * 0.43236573, 0), + * createVector(0, 0, 0) + * ); + * + * const state = createState([a, b, c]); + * + * const universe = createUniverse( + * "Universe 1", + * "rgba(254, 209, 106, 1)", + * undefined, + * state, + * createVelocityVerletSim(force), + * [] + * ); + * const universe2 = createUniverse( + * "Universe 2", + * "rgba(254, 0, 0, 1)", + * undefined, + * clone(state), + * createRungeKutta4Sim(force, [1, 2, 2, 1]), + * [] + * ); + * + * const simulation = createSimulation([universe, universe2], "3D", false, undefined, true, 100); + * playSim(simulation); + * ``` + * + * @module nbody + * @author Yeluri Ketan + */ + +export { createCelestialBody } from './CelestialBody'; +export { createCentripetalForce, createCombinedForce, createForce, createGravity } from './Force'; +export { clone } from './Misc'; +export { createExplicitEulerSim, createLambdaSim, createRungeKutta4Sim, createSemiImplicitEulerSim, createVelocityVerletSim } from './SimulateFunction'; +export { createSimulation, playSim, recordSim } from './Simulation'; +export { createState, getBodies } from './State'; +export { createBodyCenterTransformation, createCoMTransformation, createLambdaTransformation, createPinTransformation, createRotateTransformation, createTimedRotateTransformation } from './Transformation'; +export { createUniverse } from './Universe'; +export { addVectors, createVector, getX, getY, getZ, multiplyScalar, setX, setY, setZ, subVectors } from './Vector'; + diff --git a/src/tabs/Nbody/index.tsx b/src/tabs/Nbody/index.tsx new file mode 100644 index 000000000..acbe9a266 --- /dev/null +++ b/src/tabs/Nbody/index.tsx @@ -0,0 +1,220 @@ +import { Button, ButtonGroup, NumericInput } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import type { Simulation } from 'nbody'; +import React from 'react'; + +/** + * Visualize n-body simulations. + * @author Yeluri Ketan + */ + +/** + * React Component props for the Tab. + */ +type Props = { + children?: never; + className?: never; + context?: any; +}; + +/** + * React component state for the viz tab. + */ +type State = { +}; + +/** + * React component props for the control buttons. + */ +type SimControlProps = { + children?: never; + className?: never; + context?: any; + sim: Simulation; +}; + +/** + * React component state for the control buttons. + */ +type SimControlState = { + isPlaying: boolean; + speed: number; + showTrails: boolean; + showUniverse: boolean[]; +}; + +/** + * Component for UI buttons within tab e.g play/pause. + */ +class SimulationControl extends React.Component { + constructor(props) { + super(props); + this.state = { + isPlaying: false, + speed: 1, + showTrails: props.sim.getShowTrails(), + showUniverse: props.sim.universes.map(() => true), + }; + } + + toggleSimPause(): void { + const currentState = this.state.isPlaying; + this.setState({ isPlaying: !currentState }); + if (currentState) { + this.props.sim.pause(); + } else { + this.props.sim.resume(); + } + } + + toggleShowTrails(): void { + const currentState = this.state.showTrails; + this.setState({ showTrails: !currentState }); + this.props.sim.setShowTrails(!currentState); + } + + setSpeed(speed: number): void { + this.setState({ speed }); + this.props.sim.setSpeed(speed); + } + + toggleShowUniverse(label: string, i: number): void { + this.props.sim.setShowUniverse(label, !this.state.showUniverse[i]); + this.setState({ + showUniverse: this.state.showUniverse.map((v, j) => (i === j) ? !v : v) + }); + + } + + public render() { + return ( + <> + +