Skip to content

Commit

Permalink
Big changes for particle collections: Particles are now properly seri…
Browse files Browse the repository at this point in the history
…alized, and rendered as Sprites, with Single and Continuous mode being handled in two different files.#66
  • Loading branch information
AgustinVallejo committed Dec 11, 2024
1 parent 0206d2e commit 5dc758d
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 339 deletions.
72 changes: 72 additions & 0 deletions js/spin/model/MultipleParticleCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2024, University of Colorado Boulder

/**
* MultipleParticleCollection handles the nuances of the continuous mode, where particles are created at a certain rate.
*
* @author Agustín Vallejo
*/

import Tandem from '../../../../tandem/js/Tandem.js';
import quantumMeasurement from '../../quantumMeasurement.js';
import { ParticleCollection } from './ParticleCollection.js';
import SpinModel from './SpinModel.js';

const MAX_PARTICLE_CREATION_RATE = 5; // max rate of particles created per second

export class MultipleParticleCollection extends ParticleCollection {

// The fractional accumulator for the emission rate, which is used to determine how many particles to create each step.
private fractionalEmissionAccumulator = 0;

public constructor(
model: SpinModel,
maxParticles: number,
tandem: Tandem
) {
super( model, maxParticles, tandem );
}

private shootMultipleParticles(): void {
// Calculate the number of particles to produce in this time step based on the particle amount property, the max
// creation rate, and the time step. This could include a fractional amount.
const particlesToCreate = this.model.particleSourceModel.particleAmountProperty.value * MAX_PARTICLE_CREATION_RATE;

// Calculate the whole number to actually activate, and use the fractional accumlator in the process.
let wholeParticlesToCreate = Math.floor( particlesToCreate );

this.fractionalEmissionAccumulator += particlesToCreate - wholeParticlesToCreate;

if ( this.fractionalEmissionAccumulator >= 1 ) {
wholeParticlesToCreate++;
this.fractionalEmissionAccumulator -= 1;
}

// Activate the particles.
for ( let i = 0; i < wholeParticlesToCreate; i++ ) {
if ( this.particles.length < this.maxParticles ) {
this.createParticle();
}
}
}

/**
* Steps the model.
* @param dt - time step, in seconds
*/
public override step( dt: number ): void {
super.step( dt );

// Generates the stream of particles. They are activated, not created, as they are already created at construction.
this.shootMultipleParticles();

// Step all active particles, and deactivate them if they cross the exit blocker position, and step them
// normally if not.
this.particles.forEach( particle => {
particle.step( dt );
this.decideParticleDestiny( particle );
} );
}
}


quantumMeasurement.register( 'MultipleParticleCollection', MultipleParticleCollection );
238 changes: 238 additions & 0 deletions js/spin/model/ParticleCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Copyright 2024, University of Colorado Boulder

/**
* ParticleCollection is the model for particles with a predetermined spin. It has a lifetime, which will
* determine its position in the Ray Path, and a spin value, which will be modified as it passes through the SG apparatuses.
*
* @author Agustín Vallejo
*/

import dotRandom from '../../../../dot/js/dotRandom.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import PhetioObject from '../../../../tandem/js/PhetioObject.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import IOType from '../../../../tandem/js/types/IOType.js';
import ReferenceArrayIO from '../../../../tandem/js/types/ReferenceArrayIO.js';
import quantumMeasurement from '../../quantumMeasurement.js';
import { BlockingMode } from './BlockingMode.js';
import { ParticleWithSpin } from './ParticleWithSpin.js';
import { SpinDirection } from './SpinDirection.js';
import SpinModel from './SpinModel.js';
import SternGerlach from './SternGerlach.js';

const PARTICLE_RAY_WIDTH = 0.02;

const HORIZONTAL_ENDPOINT = new Vector2( 10, 0 );

export class ParticleCollection extends PhetioObject {

protected readonly maxParticles: number;

// Particles array
public readonly particles: ParticleWithSpin[];

public readonly model: SpinModel;

public constructor(
model: SpinModel,
maxParticles: number,
tandem: Tandem
) {
super( {
tandem: tandem,
phetioType: ParticleCollection.ParticleCollectionIO
} );
this.maxParticles = maxParticles;
this.model = model;
this.particles = [];
}

protected createParticle(): void {

const spinVectors = [ this.model.derivedSpinStateProperty.value.copy(), new Vector2( 0, 0 ), new Vector2( 0, 0 ) ];
const isSpinUp = [ false, false, false ];
const stageCompleted = [ false, false, false ];

const particleStartPosition = this.model.particleSourceModel.exitPositionProperty.value;
const particleEndPosition = this.model.sternGerlachs[ 0 ].entrancePositionProperty.value.plusXY(
SternGerlach.STERN_GERLACH_WIDTH, 0
);

const randomParticleOffset = new Vector2(
PARTICLE_RAY_WIDTH * ( dotRandom.nextDouble() * 2 - 1 ),
PARTICLE_RAY_WIDTH * ( dotRandom.nextDouble() * 2 - 1 )
);

const position = particleStartPosition.plus( randomParticleOffset );

const particle = new ParticleWithSpin(
0, // Lifetime starts at 0
position,
Vector2.ZERO, // Velocity starts at 0, will be set in the constructor
spinVectors,
isSpinUp,
stageCompleted,
particleStartPosition,
particleEndPosition,
randomParticleOffset
);

this.particles.push( particle );
}

public removeParticle( particle: ParticleWithSpin ): void {
_.pull( this.particles, particle );
}

public clear(): void {
this.particles.length = 0;
}

/**
* Steps the model.
* @param dt - time step, in seconds
*/
public step( dt: number ): void {
// Remove old particles
this.particles.forEach( particle => {
if ( particle.lifetime > 4 ) {
this.removeParticle( particle );
}
} );
}


/**
* Check if the particle would be blocked by the exit blocker, and if so, reset it and return true.
*/
protected checkParticleBlocking( particle: ParticleWithSpin ): boolean {
// If there is no blocker in place
if ( this.model.sternGerlachs[ 0 ].blockingModeProperty.value !== BlockingMode.NO_BLOCKER ) {
const exitPositionProperty = this.model.exitBlockerPositionProperty.value;
if (
// If the blocker is on the 'up' position and the particle is spin up, and blocker position exists (can be null),
// and particle position is greater than the blocker position
this.model.sternGerlachs[ 0 ].blockingModeProperty.value === BlockingMode.BLOCK_UP &&
particle.isSpinUp[ 1 ] && exitPositionProperty &&
( particle.position.x > exitPositionProperty.x ) ) {
this.removeParticle( particle );
return true;
}
else if (
this.model.sternGerlachs[ 0 ].blockingModeProperty.value === BlockingMode.BLOCK_DOWN &&
!particle.isSpinUp[ 1 ] && exitPositionProperty &&
( particle.position.x > exitPositionProperty.x ) ) {
this.removeParticle( particle );
return true;
}
}
return false;
}

protected decideParticleDestiny( particle: ParticleWithSpin ): void {
const wasParticleBlocked = this.checkParticleBlocking( particle );
if ( wasParticleBlocked ) {
return;
}

const threshold = 0.03;

// If the particle were to reach its end position, measure it and decide on a new path
if ( particle.position.x > particle.endPosition.x - threshold ) {
const extraDistance = particle.position.x - particle.endPosition.x;
const extraTime = extraDistance / particle.speed;

const usingSingleApparatus = this.model.currentExperimentProperty.value.usingSingleApparatus;

// If the first stage SOURCE-SG0 is not yet completed, mark it as completed and measure the particle
if ( !particle.stageCompleted[ 0 ] ) {
const isResultUp = this.measureParticle( particle, this.model.sternGerlachs[ 0 ], 1, particle.spinVectors[ 0 ] );
this.model.sternGerlachs[ 0 ].count( isResultUp );

const startPosition = isResultUp ?
this.model.sternGerlachs[ 0 ].topExitPositionProperty.value :
this.model.sternGerlachs[ 0 ].bottomExitPositionProperty.value;
if ( usingSingleApparatus ) {
const endPosition = isResultUp ?
this.model.sternGerlachs[ 0 ].topExitPositionProperty.value.plus( HORIZONTAL_ENDPOINT ) : // To infinity
this.model.sternGerlachs[ 0 ].bottomExitPositionProperty.value.plus( HORIZONTAL_ENDPOINT ); // To infinity
particle.updatePath( startPosition, endPosition, extraTime );
}
else {
const endPosition = isResultUp ?
this.model.sternGerlachs[ 1 ].entrancePositionProperty.value :
this.model.sternGerlachs[ 2 ].entrancePositionProperty.value;
particle.updatePath( startPosition, endPosition, extraTime );
}

particle.stageCompleted[ 0 ] = true;

}
else if ( !usingSingleApparatus && !particle.stageCompleted[ 1 ] ) {
const startPosition = particle.endPosition;
const endPosition = particle.endPosition.plusXY(
SternGerlach.STERN_GERLACH_WIDTH, 0
);
particle.updatePath( startPosition, endPosition, extraTime );

particle.stageCompleted[ 1 ] = true;
}
else if ( !usingSingleApparatus && !particle.stageCompleted[ 2 ] ) {
const sternGerlach = particle.isSpinUp[ 1 ] ? this.model.sternGerlachs[ 1 ] : this.model.sternGerlachs[ 2 ];
const isResultUp = this.measureParticle(
particle, sternGerlach, 2, particle.spinVectors[ 1 ] );
sternGerlach.count( isResultUp );

const startPosition = isResultUp ?
sternGerlach.topExitPositionProperty.value :
sternGerlach.bottomExitPositionProperty.value;
const endPosition = particle.startPosition.plus( HORIZONTAL_ENDPOINT ); // To infinity
particle.updatePath( startPosition, endPosition, extraTime );

particle.stageCompleted[ 2 ] = true;
}
}
}

/**
* Given the incoming state of a particle, calculate the result of a SG measurement on a particle and set its spin
*/
protected measureParticle(
particle: ParticleWithSpin,
sternGerlach: SternGerlach,
experimentStageIndex: number,
incomingState: Vector2 ): boolean {

const upProbability = sternGerlach.calculateProbability( incomingState );
const isResultUp = dotRandom.nextDouble() < upProbability;
particle.isSpinUp[ experimentStageIndex ] = isResultUp;
particle.spinVectors[ experimentStageIndex ].set( SpinDirection.spinToVector(
isResultUp ?
sternGerlach.isZOrientedProperty.value ? SpinDirection.Z_PLUS : SpinDirection.X_PLUS :
sternGerlach.isZOrientedProperty.value ? SpinDirection.Z_MINUS : null
) );
return isResultUp;
}

/**
* For serialization, the ParticleCollectionIO uses reference type serialization. That is, each ParticleCollection exists for the life of the
* simulation, and when we save the state of the simulation, we save the current state of the ParticleCollection.
*
* The ParticleCollection serves as a composite container of ParticleIO instances. The Particles are serialized using data-type serialization.
* For deserialization, the Particles are deserialized (again, using data-type serialization) and applied to the
* ParticleCollection in its applyState method.
*
* Please see https://github.com/phetsims/phet-io/blob/main/doc/phet-io-instrumentation-technical-guide.md#serialization
* for more information on the different serialization types.
*/
public static readonly ParticleCollectionIO = new IOType<ParticleCollection>( 'ParticleCollectionIO', {
valueType: ParticleCollection,
documentation: 'The ParticleCollection is a model element that represents a collection of photons.',
stateSchema: {
particles: ReferenceArrayIO( ParticleWithSpin.ParticleWithSpinIO )
}
} );
}


quantumMeasurement.register( 'ParticleCollection', ParticleCollection );
2 changes: 1 addition & 1 deletion js/spin/model/ParticleSourceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default class ParticleSourceModel {
this.exitLocalPosition = new Vector2( 0, 0 );

this.exitPositionProperty = new DerivedProperty( [ this.positionProperty ], ( position: Vector2 ) => {
return position.plus( this.exitLocalPosition );
return position.plus( this.exitLocalPosition ).plusXY( ParticleSourceModel.PARTICLE_SOURCE_WIDTH / 2, 0 );
} );

const initialSpinState = SpinDirection.Z_PLUS;
Expand Down
Loading

0 comments on commit 5dc758d

Please sign in to comment.