๐๏ธ Architecture
Central concepts
Section titled โCentral conceptsโIn physics and mathematics, we are always searching for the most fundamental abstractions:
A state can may change in time. This change in time can be thought of as applying an operator to that 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
Realisation in Helion
Section titled โRealisation in Helionโfield.apply(operator);field.evolve(solver, dt);field.bind(view);apply โ transformatieevolve โ dynamicasynchronize โ visualisatieJa, 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:
Instantane operatoren
Section titled โInstantane operatorenโfield.apply(new GaussianImpulse(...));field.apply(new FFT2D());field.apply(new Blur(...));Dit zijn directe transformaties:
Geen tijd, geen geschiedenis.
Evolutie-operatoren
Section titled โEvolutie-operatorenโfield.evolve(solver, dt);body.evolve(integrator, dt);Dit zijn tijdstappen:
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:
| Concept | Betekenis |
|---|---|
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):
en je hebt een evolutie-operator (E_{\Delta 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:
DiscreteScalarFieldDiscreteComplexFieldRadialSymmetricBodyParticleSystemMeshdan 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.
DiscreteScalarFieldDiscreteComplexFieldRadialSymmetricBodyDit zijn toestanden.
Operator
Section titled โOperatorโFFT2DFFTShift2DGaussianImpulseDoubleSlitBlurDit zijn transformaties.
Equation
Section titled โEquationโWaveEquationSchrodingerEquationHeatEquationDit zijn wetten.
WaveEquationSolverSchrodingerSolverDit 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:
or
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 FieldParticle extends FieldSurface extends FieldMesh extends FieldDat 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 Fieldmaar:
Everything is Stateen
State is transformed by OperatorsDat 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.
Declarative coding
Section titled โDeclarative codingโ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 resultFields and operators
Section titled โFields and operatorsโDiscreteScalarFieldDiscreteComplexFieldCirclePotentialSquarePotentialLinePotentialStepPotentialSingleSlitDoubleSlitGratingBlurFFT2DFFTShift2DGaussianImpulseComplex2DThis 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 solverSolver updates fieldSurfaceView samples fieldnumerics/โโโ solvers/โ โโโ JacobiSolverโ โโโ GaussSeidelSolverโ โโโ MultigridSolverโ โโโ FiniteDifferenceWaveSolverโโโโ operators/โ โโโ Laplacian2Dโ โโโ Gradient2Dโ โโโ Divergence2Dโโโโ boundaryconditions/โ โโโ Dirichletโ โโโ Neumannโ โโโ PeriodicSurfaceView โSurface.sample() โScalarField.scalarValueAt()const field = new ScalarGridField(...);
const solver = new WaveEquationSolver({ field, boundaryCondition: BoundaryCondition.Dirichlet});
simulation.onClockTick((_, dt) => solver.step(dt));Introduction
Section titled โIntroductionโIn jouw architectuur (zoals je die nu gebruikt)
Je hebt 3 lagen:
- Physics layer Dipole, Particle, VectorField
โ puur fysica in meters (of SI units)
- Simulation layer Simulation
โ tijd, substeps, integratie
โ hoort NIETS te weten over visual scale
- Render layer ThreeJsRenderer._world ArrowField2 Sphere etc.
โ mapping physics โ visuals
Getting started
Section titled โGetting startedโ๐ My first simulation
Section titled โ๐ My first simulationโTODO
The physics layer
Section titled โThe physics layerโThe simulation layer
Section titled โThe simulation layerโThe rendering layer
Section titled โThe rendering layerโSurfaces
Section titled โSurfacesโ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