Skip to content

๐Ÿ›๏ธ Architecture

In physics and mathematics, we are always searching for the most fundamental abstractions:

stateโ†’operatorstate\text{state} \xrightarrow{\text{operator}} \text{state}

A state can may change in time. This change in time can be thought of as applying an operator to that state:

ddtstate=L(state)\frac{d}{dt} \text{state} = \mathcal{L}(\text{state})

Eventually, everything may thus be conceptualized as states and operators.

However, software adds additional dimensions that are unknown to math and physics:

  • code should express intent (Kent Beckโ€™s rules of simple design)
  • performance (fields are refreshed dozens of times per second)
  • memory management (fields with thousands pixels/vertices)
  • maintenance

As a consequence, the mathematical most elegant abstraction is not always tantamount to the best programming abstraction. So Helion is built with the following doctrine in mind:

Unify concepts, but not necessarily the syntax

field.apply(operator);
field.evolve(solver, dt);
field.bind(view);
apply โ†’ transformatie
evolve โ†’ dynamica
synchronize โ†’ visualisatie

Ja, dat vind ik eigenlijk een veel sterkere unificatie dan field.apply(solver).

Als ik naar de voorbeelden kijk die je hebt laten zien, dan zie ik twee fundamenteel verschillende soorten transformaties:

field.apply(new GaussianImpulse(...));
field.apply(new FFT2D());
field.apply(new Blur(...));

Dit zijn directe transformaties:

xโ†ฆO(x)x \mapsto O(x)

Geen tijd, geen geschiedenis.


field.evolve(solver, dt);
body.evolve(integrator, dt);

Dit zijn tijdstappen:

x(t)โ†ฆx(t+ฮ”t)x(t) \mapsto x(t+\Delta t)

Daar zit expliciet tijd in.


Vanuit de theoretische natuurkunde is dat onderscheid ook heel natuurlijk.

Een FFT is geen tijdsevolutie.

Een impuls is geen tijdsevolutie.

Een symplectische Euler-stap is wรฉl een tijdsevolutie.

Een Schrรถdinger-solver is wรฉl een tijdsevolutie.


Daardoor krijg je een mooie grammatica:

state.apply(operator);
state.evolve(solver, dt);

waarbij:

ConceptBetekenis
apply()algebraรฏsche / geometrische transformatie
evolve()System dynamics

Voor een veld:

psi
.apply(new GaussianImpulse(...))
.evolve(new SchrodingerSolver(eq), dt);

Voor een lichaam:

body
.apply(force)
.evolve(new SymplecticEulerIntegrator(), dt);

of misschien:

body.evolve(
new SymplecticEulerIntegrator(forceField),
dt
);

afhankelijk van hoe je krachten modelleert.


Het mooie is dat de semantiek dan overeind blijft.

Ik zou persoonlijk wat huiverig zijn voor:

body.apply(new SymplecticEulerIntegrator(), dt);

want een integrator is geen operator op een toestand.

Een integrator is een procedure die een toestand door de tijd verplaatst.

evolve() drukt dat veel beter uit.


Nog iets interessants: hiermee krijg je bijna letterlijk de structuur uit de dynamische systeemtheorie.

Je hebt een toestand (x).

Je hebt operatoren (O):

xโ†O(x)x \leftarrow O(x)

en je hebt een evolutie-operator (E_{\Delta t}):

x(t+ฮ”t)=Eฮ”t(x(t))x(t+\Delta t) = E_{\Delta t}(x(t))

Dat vertaalt zich direct naar:

state.apply(operator);
state.evolve(solver, dt);

Dat is een heel compacte en mathematisch herkenbare API.


Sterker nog, als je later meerdere soorten toestand krijgt:

DiscreteScalarField
DiscreteComplexField
RadialSymmetricBody
ParticleSystem
Mesh

dan kun je ze allemaal dezelfde โ€œtaalโ€ laten spreken:

state.apply(operator);
state.evolve(solver, dt);
state.alwaysWith(view);

Dat vind ik eerlijk gezegd een van de elegantste richtingen voor Helion die ik tot nu toe in je voorbeelden heb gezien. Het behoudt de semantische verschillen tussen operatoren en tijdintegratie, maar geeft ze wel een uniforme vorm. Dat is meestal een beter resultaat dan proberen alles onder รฉรฉn enkele methode (apply) te forceren.

DiscreteScalarField
DiscreteComplexField
RadialSymmetricBody

Dit zijn toestanden.


FFT2D
FFTShift2D
GaussianImpulse
DoubleSlit
Blur

Dit zijn transformaties.


WaveEquation
SchrodingerEquation
HeatEquation

Dit zijn wetten.


WaveEquationSolver
SchrodingerSolver

Dit zijn numerieke procedures.


Vanuit theoretische natuurkunde zou je kunnen zeggen:

een solver is ook een operator

en dat is eigenlijk waar.

Een solver realiseert immers een discrete tijdsevolutie-operator:

ฯˆ(t+ฮ”t)=U(ฮ”t)ฯˆ(t)\psi(t+\Delta t) = U(\Delta t)\psi(t)

or

ฯ•n+1=Sฮ”t(ฯ•n)\phi_{n+1} = S_{\Delta t}(\phi_n)

Dus conceptueel is een solver inderdaad een operator.


Waar ik denk dat jouw huidige ontwerp wringt is niet dat de solver geen operator is.

Het wringt omdat de solver momenteel eigenaar is van de toestand:

const solver =
new WaveEquationSolver(field, equation);

Terwijl overal elders in Helion de toestand eigenaar blijft van zichzelf:

field.apply(operator);

of

body.apply(force);

Ik denk daarom dat deze richting heel natuurlijk zou zijn:

const solver =
new WaveEquationSolver(equation);
solver.step(field, dt);

of zelfs:

field.evolve(solver, dt);

waarbij de solver geen veld bewaart.


Het mooie daarvan is dat je dan ook meerdere velden met dezelfde solver kunt evolueren:

const solver =
new SchrodingerSolver(equation);
solver.step(psi1, dt);
solver.step(psi2, dt);
solver.step(psi3, dt);

Dat is conceptueel vaak zuiverder.


Wat ik nรญet zou doen is teruggaan naar je oorspronkelijke idee:

alles is een Field

Dat zie ik veel mensen proberen.

Bijvoorbeeld:

Body extends Field
Particle extends Field
Surface extends Field
Mesh extends Field

Dat levert meestal een prachtige theorie op en een onhandige codebase.

Je verliest dan precies wat jij noemt: semantiek.

Een RadialSymmetricBody is geen veld. Een lichaam heeft massa, impuls, traagheid, botsingen, enzovoort.

Een DiscreteComplexField is geen lichaam.

Dat ze beide toestanden zijn betekent niet dat ze hetzelfde type moeten zijn.


Wat ik uit jouw huidige ontwerp haal is eigenlijk iets subtielers:

Niet:

Everything is a Field

maar:

Everything is State

en

State is transformed by Operators

Dat is een veel krachtigere unificatie.

Dan mogen er best meerdere soorten toestand bestaan:

  • velden
  • lichamen
  • geometrieรซn
  • grafen
  • meshes

zolang ze allemaal dezelfde operationele grammatica delen:

state.apply(operator);

en

state.alwaysWith(view);

Dat lijkt mij een betere balans tussen de elegantie van de theoretische natuurkunde en de praktische eisen van softwareontwerp. Dat is ook precies de richting waarin je Fourier-refactor je nu lijkt te duwen.

field // E.g. a discrete scalar field
.reset() // Start with a blank slate
.apply(new GaussianImpluse()) // Apply a Gaussian impulse
.apply(new FFT2D()) // Apply a Fourier transform
.apply(new FFTShift2D()); // Shift the result
DiscreteScalarField
DiscreteComplexField
CirclePotential
SquarePotential
LinePotential
StepPotential
SingleSlit
DoubleSlit
Grating
Blur
FFT2D
FFTShift2D
GaussianImpulseComplex2D

This takes care of a declarative simulation

potential
.reset()
.apply(new DoubleSlit({
size: 40,
energy: 0.1
}))
.apply(new Blur(4));

Ik zou daarom tijdens deze refactoring proberen รฉรฉn ontwerpprincipe heel consequent vast te houden:

Een model wordt precies รฉรฉn keer aan een visualisatie gekoppeld. Daarna mag de visualisatie intern alles veranderen zonder dat de binding of de simulator daarvan iets merkt.

Dat principe maakt niet alleen de huidige surface-weergaven netjes, maar vormt ook een sterke basis voor de probabilistische orbitalen, vectorvelden, volumerendering en andere visualisaties die je later wilt toevoegen.

Wat je nu hebt lijkt sterk op een kleine ECS/MVC-hybride, en dat schaalt veel beter dan losse imperative canvas-code.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Mathematical Layer โ”‚
โ”‚ โ”‚
โ”‚ ScalarField โ”‚
โ”‚ VectorField โ”‚
โ”‚ ParametricSurface โ”‚
โ”‚ DifferentialGeometry โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Discretization Layer โ”‚
โ”‚ โ”‚
โ”‚ Range โ”‚
โ”‚ SurfaceResolution โ”‚
โ”‚ Sampling (u,v grids) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ View Layer โ”‚
โ”‚ โ”‚
โ”‚ PlaneSurfaceView โ”‚
โ”‚ IsoparametricContoursView โ”‚
โ”‚ ArrowField โ”‚
โ”‚ PointCloudView โ”‚
โ”‚ ScalarFieldSurface โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Rendering Layer โ”‚
โ”‚ โ”‚
โ”‚ ThreeJsRenderer โ”‚
โ”‚ InstancedMesh โ”‚
โ”‚ Materials / Shaders โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Simulation Layer โ”‚
โ”‚ โ”‚
โ”‚ Simulation loop โ”‚
โ”‚ EventController โ”‚
โ”‚ Time evolution โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

MVC:

Simulation
updates solver
Solver
updates field
SurfaceView
samples field
numerics/
โ”œโ”€โ”€ solvers/
โ”‚ โ”œโ”€โ”€ JacobiSolver
โ”‚ โ”œโ”€โ”€ GaussSeidelSolver
โ”‚ โ”œโ”€โ”€ MultigridSolver
โ”‚ โ””โ”€โ”€ FiniteDifferenceWaveSolver
โ”‚
โ”œโ”€โ”€ operators/
โ”‚ โ”œโ”€โ”€ Laplacian2D
โ”‚ โ”œโ”€โ”€ Gradient2D
โ”‚ โ””โ”€โ”€ Divergence2D
โ”‚
โ”œโ”€โ”€ boundaryconditions/
โ”‚ โ”œโ”€โ”€ Dirichlet
โ”‚ โ”œโ”€โ”€ Neumann
โ”‚ โ””โ”€โ”€ Periodic
SurfaceView
โ†“
Surface.sample()
โ†“
ScalarField.scalarValueAt()
const field = new ScalarGridField(...);
const solver = new WaveEquationSolver({
field,
boundaryCondition: BoundaryCondition.Dirichlet
});
simulation.onClockTick((_, dt) => solver.step(dt));

In jouw architectuur (zoals je die nu gebruikt)

Je hebt 3 lagen:

  1. Physics layer Dipole, Particle, VectorField

โ†’ puur fysica in meters (of SI units)

  1. Simulation layer Simulation

โ†’ tijd, substeps, integratie

โ— hoort NIETS te weten over visual scale

  1. Render layer ThreeJsRenderer._world ArrowField2 Sphere etc.

โ†’ mapping physics โ†’ visuals

TODO

Surface
โ†“
ScalarField (ruwe waarde)
โ†“
NormalizedScalarField (altijd [0,1])
โ†“
ColorMapper (blind voor fysica)
โ†“
View (alleen rendering)
/**
* Scalar field on a surface.
* โ”‚
* โ”œโ”€โ”€ MeanCurvatureField
* โ”œโ”€โ”€ GaussianCurvatureField
* โ”œโ”€โ”€ PrincipalCurvatureField
* โ”œโ”€โ”€ GeodesicDistanceField
* โ”œโ”€โ”€ UserDefinedField
*/
export class SurfaceScalarField