Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce the number of phet-io elements by reducing the number of individually instrumented photons and particles #66

Closed
jbphet opened this issue Nov 27, 2024 · 6 comments

Comments

@jbphet
Copy link
Collaborator

jbphet commented Nov 27, 2024

The current version of this sim has a larger number of phet-io elements than most phet-io-instrumented sims. Here's a screenshot showing the count in Studio:

image

24,960 is apparently a lot. By comparison, the recently published "Mean: Share and Balance" sim has 2,941, almost exactly a factor of 10 fewer elements.

image

This relatively high number in "Quantum Measurement" is likely due to the fact that the current code instruments all of the photons and particles. While this works nicely for state, it is a problem for Studio performance.

This was discussed in an impromptu meeting today with @AgustinVallejo, @arouinfar, and me, and it was suggested that we pursue reducing this number by grouping the photons and particles into collections that are essentially a single phet-io element. @arouinfar said that Projectile Data Lab had done something similar to this. I did a little poking around in that sim's code, and it looks like the classes Projectile and Field have set up a pattern for this sort of thing that we may be able to emulate.

@AgustinVallejo
Copy link
Contributor

I started basing this off Projectile and Field, and spoke a bit with SR, who gave me good guidance. Below is a first attempt at this but it's very incomplete and I didn't want to keep meddling too much with it before dicussing with JB, but I do think is a step in the right direction.

Essentially, I created a PhotonSystem class which is the actual PhetioObject, and it uses reference type serialization to get and set the photons array. SR also mentioned we wouldn't need to preallocate the photons anymore.

This will be interlinked with #65, and the introduction of the QuantumPossibleState class can possibly make this a bit more complicated.

Subject: [PATCH] First draft of photon serialization
---
Index: js/photons/model/PhotonSystem.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/PhotonSystem.ts b/js/photons/model/PhotonSystem.ts
new file mode 100644
--- /dev/null	(date 1733342184303)
+++ b/js/photons/model/PhotonSystem.ts	(date 1733342184303)
@@ -0,0 +1,79 @@
+// Copyright 2024, University of Colorado Boulder
+
+/**
+ * PhotonSystem is a model element that represents a collection of photons.  It is used to manage the pool of photons
+ * and serialize the individual photons.
+ *
+ * @author Agustín Vallejo
+ */
+
+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 Photon, { PhotonStateObject, QuantumPossibleState } from './Photon.js';
+
+// The number of photons created at construction time and pooled for usage in the experiment.  This value was
+// empirically determined to be sufficient to handle the maximum number of photons that could be active at any given
+// time, but may have to be adjusted if other aspects of the model are changed.
+const MAX_PHOTONS = 800;
+
+export class PhotonSystem extends PhetioObject {
+
+  public readonly photons: Photon[] = [];
+
+  public constructor( tandem: Tandem ) {
+    super( {
+      tandem: tandem,
+      phetioType: PhotonSystem.PhotonSystemIO
+    } );
+
+    _.times( MAX_PHOTONS, index => {
+      const photon = new Photon(
+        false,
+        0,
+        [
+          new QuantumPossibleState( 'vertical' ),
+          new QuantumPossibleState( 'horizontal' )
+        ]
+      );
+      this.photons.push( photon );
+    } );
+  }
+
+  public getActivePhotons(): Photon[] {
+    return this.photons.filter( photon => photon.isActive );
+  }
+
+  /**
+   * For serialization, the PhotonSystemIO uses reference type serialization. That is, each PhotonSystem exists for the life of the
+   * simulation, and when we save the state of the simulation, we save the current state of the PhotonSystem.
+   *
+   * The PhotonSystem serves as a composite container of PhotonIO instances. The Photons are serialized using data-type serialization.
+   * For deserialization, the Photons are deserialized (again, using data-type serialization) and applied to the
+   * PhotonSystem 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 PhotonSystemIO = new IOType<PhotonSystem>( 'PhotonSystemIO', {
+    valueType: PhotonSystem,
+    documentation: 'The PhotonSystem is a model element that represents a collection of photons.',
+    stateSchema: {
+      photons: ReferenceArrayIO( Photon.PhotonIO )
+    }
+  } );
+
+  private toStateObject(): PhotonSystemStateObject {
+    return {
+      photons: this.photons.map( photon => Photon.PhotonIO.toStateObject( photon ) )
+    };
+  }
+}
+
+type PhotonSystemStateObject = {
+  photons: PhotonStateObject[];
+};
+
+quantumMeasurement.register( 'Photon', Photon );
\ No newline at end of file
Index: js/photons/model/Photon.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/Photon.ts b/js/photons/model/Photon.ts
--- a/js/photons/model/Photon.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/Photon.ts	(date 1733342445895)
@@ -7,13 +7,16 @@
  * @author John Blanco, PhET Interactive Simulations
  */
 
-import BooleanProperty from '../../../../axon/js/BooleanProperty.js';
 import NumberProperty from '../../../../axon/js/NumberProperty.js';
 import Utils from '../../../../dot/js/Utils.js';
 import Vector2 from '../../../../dot/js/Vector2.js';
 import Vector2Property from '../../../../dot/js/Vector2Property.js';
-import PhetioObject from '../../../../tandem/js/PhetioObject.js';
-import Tandem from '../../../../tandem/js/Tandem.js';
+import BooleanIO from '../../../../tandem/js/types/BooleanIO.js';
+import IOType from '../../../../tandem/js/types/IOType.js';
+import NumberIO from '../../../../tandem/js/types/NumberIO.js';
+import ReferenceArrayIO from '../../../../tandem/js/types/ReferenceArrayIO.js';
+import { ReferenceIOState } from '../../../../tandem/js/types/ReferenceIO.js';
+import StringIO from '../../../../tandem/js/types/StringIO.js';
 import quantumMeasurement from '../../quantumMeasurement.js';
 
 export const PHOTON_SPEED = 0.3; // meters per second
@@ -30,25 +33,19 @@
 
 // TODO: This class could live in its own file, once the feature is fully green lit, will move https://github.com/phetsims/quantum-measurement/issues/63
 /**
- * PhotonState is a class that represents a possible state of a photon at a given point in time.
+ * QuantumPossibleState is a class that represents a possible state of a photon at a given point in time.
  * It contains properties for position, direction and the probability of the photon being in that state.
  */
-export class PhotonState {
+export class QuantumPossibleState {
   public readonly positionProperty: Vector2Property;
   public readonly directionProperty: Vector2Property;
   public readonly probabilityProperty: NumberProperty;
-  public readonly polarization: possiblePolarizationResult;
+  public polarization: possiblePolarizationResult;
 
-  public constructor( polarization: possiblePolarizationResult, tandem: Tandem ) {
-    this.positionProperty = new Vector2Property( Vector2.ZERO, {
-      tandem: tandem.createTandem( 'positionProperty' )
-    } );
-    this.directionProperty = new Vector2Property( RIGHT, {
-      tandem: tandem.createTandem( 'directionProperty' )
-    } );
-    this.probabilityProperty = new NumberProperty( polarization === 'vertical' ? 1 : 0, {
-      tandem: tandem.createTandem( 'probabilityProperty' )
-    } );
+  public constructor( polarization: possiblePolarizationResult ) {
+    this.positionProperty = new Vector2Property( Vector2.ZERO );
+    this.directionProperty = new Vector2Property( RIGHT );
+    this.probabilityProperty = new NumberProperty( polarization === 'vertical' ? 1 : 0 );
     this.polarization = polarization;
   }
 
@@ -80,39 +77,55 @@
   public step( dt: number ): void {
     this.positionProperty.set( this.positionProperty.value.plus( this.directionProperty.value.timesScalar( PHOTON_SPEED * dt ) ) );
   }
+
+  public static readonly QuantumPossibleStateIO = new IOType<QuantumPossibleState, QuantumPossibleStateStateObject>( 'QuantumPossibleStateIO', {
+    valueType: QuantumPossibleState,
+    stateSchema: {
+      position: Vector2.Vector2IO,
+      direction: Vector2.Vector2IO,
+      probability: NumberIO,
+      polarization: StringIO
+    },
+    applyState: ( quantumPossibleState: QuantumPossibleState, stateObject: QuantumPossibleStateStateObject ) => {
+      quantumPossibleState.positionProperty.set( Vector2.Vector2IO.fromStateObject( stateObject.position ) );
+      quantumPossibleState.directionProperty.set( Vector2.Vector2IO.fromStateObject( stateObject.direction ) );
+      quantumPossibleState.probabilityProperty.set( stateObject.probability );
+      quantumPossibleState.polarization = stateObject.polarization as possiblePolarizationResult;
+      console.log("Applying State!");
+    }
+  } );
+
+  public toStateObject(): QuantumPossibleStateStateObject {
+    console.log("Setting state!");
+    return {
+      position: Vector2.Vector2IO.toStateObject( this.positionProperty ),
+      direction: Vector2.Vector2IO.toStateObject( this.positionProperty ),
+      probability: this.probabilityProperty.value,
+      polarization: this.polarization
+    };
+  }
 }
 
-export default class Photon extends PhetioObject {
+export default class Photon {
+
+  // whether this photon is active, and should thus be moved by the model and shown in the view
+  public isActive: boolean;
+
+  // the angle of polarization for this photon, in degrees
+  public polarizationAngle: number;
 
   // Contains all the possible states of the photon, which include position, direction, and probability.
   // Since they contain properties, and based on the design of this simulation, it will always have two states.
-  public possibleStates: [ PhotonState, PhotonState ];
-
-  // whether this photon is active, and should thus be moved by the model and shown in the view
-  public readonly activeProperty: BooleanProperty;
-
-  // the angle of polarization for this photon, in degrees
-  public readonly polarizationAngleProperty: NumberProperty;
-
-  public constructor( tandem: Tandem ) {
-
-    super( {
-      tandem: tandem,
-      phetioState: false
-    } );
+  public possibleStates: QuantumPossibleState[];
 
-    this.polarizationAngleProperty = new NumberProperty( 0, {
-      tandem: tandem.createTandem( 'polarizationAngleProperty' )
-    } );
-
-    this.activeProperty = new BooleanProperty( false, {
-      tandem: tandem.createTandem( 'activeProperty' )
-    } );
-
-    this.possibleStates = [
-      new PhotonState( 'vertical', tandem.createTandem( 'verticalState' ) ),
-      new PhotonState( 'horizontal', tandem.createTandem( 'horizontalState' ) )
-    ];
+  public constructor(
+    isActive: boolean,
+    polarizationAngle: number,
+    possibleStates: [ QuantumPossibleState, QuantumPossibleState ]
+  ) {
+    this.isActive = isActive;
+    this.polarizationAngle = polarizationAngle;
+    this.possibleStates = possibleStates;
 
     // Entangle the possible states
     this.possibleStates[ 0 ].probabilityProperty.lazyLink( probability => {
@@ -125,21 +138,53 @@
   }
 
   public step( dt: number ): void {
-    if ( this.activeProperty.value ) {
+    if ( this.isActive ) {
       this.possibleStates.forEach( state => {
         state.step( dt );
       } );
     }
   }
 
-  public reset(): void {
-    this.activeProperty.reset();
-    this.possibleStates.forEach( state => {
-      state.positionProperty.reset();
-      state.directionProperty.reset();
-      state.probabilityProperty.reset();
-    } );
-  }
+
+  /**
+   * Individual Projectile instances are not PhET-iO Instrumented. Instead, the Field that contains the Projectiles
+   * calls ProjectileIO.toStateObject to serialize the Projectile instances. FieldIO uses reference type serialization
+   * as a composite of the Projectiles, which use data type serialization.
+   *
+   * 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 PhotonIO = new IOType<Photon, PhotonStateObject>( 'PhotonIO', {
+    valueType: Photon,
+    stateSchema: {
+      active: BooleanIO,
+      polarizationAngle: NumberIO,
+      possibleStates: ReferenceArrayIO( QuantumPossibleState.QuantumPossibleStateIO )
+    },
+    applyState: ( photon: Photon, stateObject: PhotonStateObject ) => {
+      photon.isActive = stateObject.active;
+      photon.polarizationAngle = stateObject.polarizationAngle;
+      photon.possibleStates = stateObject.possibleStates.map( stateObject => {
+        const quantumPossibleState = new QuantumPossibleState( stateObject.polarization as possiblePolarizationResult );
+        QuantumPossibleState.QuantumPossibleStateIO.applyState( quantumPossibleState, stateObject );
+        return quantumPossibleState;
+      } );
+    }
+  } );
 }
 
+
+export type PhotonStateObject = {
+  active: boolean;
+  polarizationAngle: number;
+  possibleStates: QuantumPossibleStateStateObject[];
+};
+
+export type QuantumPossibleStateStateObject = {
+  position: ReferenceIOState;
+  direction: ReferenceIOState;
+  probability: number;
+  polarization: string;
+};
+
 quantumMeasurement.register( 'Photon', Photon );
\ No newline at end of file
Index: js/photons/model/Mirror.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/Mirror.ts b/js/photons/model/Mirror.ts
--- a/js/photons/model/Mirror.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/Mirror.ts	(date 1733338110580)
@@ -13,7 +13,7 @@
 import PickRequired from '../../../../phet-core/js/types/PickRequired.js';
 import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js';
 import quantumMeasurement from '../../quantumMeasurement.js';
-import Photon, { DOWN, PhotonState } from './Photon.js';
+import Photon, { DOWN, QuantumPossibleState } from './Photon.js';
 import { PhotonInteractionTestResult } from './PhotonsModel.js';
 import { TPhotonInteraction } from './TPhotonInteraction.js';
 
@@ -40,7 +40,7 @@
     this.mirrorSurfaceLine = new Line( endpoint1, endpoint2 );
   }
 
-  public testForPhotonInteraction( photonState: PhotonState, photon: Photon, dt: number ): PhotonInteractionTestResult {
+  public testForPhotonInteraction( photonState: QuantumPossibleState, photon: Photon, dt: number ): PhotonInteractionTestResult {
 
     assert && assert( photon.activeProperty.value, 'save CPU cycles - don\'t use this method with inactive photons' );
 
Index: js/photons/model/TPhotonInteraction.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/TPhotonInteraction.ts b/js/photons/model/TPhotonInteraction.ts
--- a/js/photons/model/TPhotonInteraction.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/TPhotonInteraction.ts	(date 1733338110585)
@@ -6,9 +6,9 @@
  * @author John Blanco, PhET Interactive Simulations
  */
 
-import Photon, { PhotonState } from './Photon.js';
+import Photon, { QuantumPossibleState } from './Photon.js';
 import { PhotonInteractionTestResult } from './PhotonsModel.js';
 
 export type TPhotonInteraction = {
-  testForPhotonInteraction( state: PhotonState, photon: Photon, dt: number ): PhotonInteractionTestResult;
+  testForPhotonInteraction( state: QuantumPossibleState, photon: Photon, dt: number ): PhotonInteractionTestResult;
 };
\ No newline at end of file
Index: js/photons/model/PolarizingBeamSplitter.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/PolarizingBeamSplitter.ts b/js/photons/model/PolarizingBeamSplitter.ts
--- a/js/photons/model/PolarizingBeamSplitter.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/PolarizingBeamSplitter.ts	(date 1733338110589)
@@ -16,7 +16,7 @@
 import PickRequired from '../../../../phet-core/js/types/PickRequired.js';
 import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js';
 import quantumMeasurement from '../../quantumMeasurement.js';
-import Photon, { PhotonState, RIGHT, UP } from './Photon.js';
+import Photon, { QuantumPossibleState, RIGHT, UP } from './Photon.js';
 import { PhotonInteractionTestResult } from './PhotonsModel.js';
 import { TPhotonInteraction } from './TPhotonInteraction.js';
 
@@ -49,7 +49,7 @@
     this.polarizingSurfaceLine = new Line( endpoint1, endpoint2 );
   }
 
-  public testForPhotonInteraction( photonState: PhotonState, photon: Photon, dt: number ): PhotonInteractionTestResult {
+  public testForPhotonInteraction( photonState: QuantumPossibleState, photon: Photon, dt: number ): PhotonInteractionTestResult {
 
     assert && assert( photon.activeProperty.value, 'save CPU cycles - don\'t use this method with inactive photons' );
 
Index: js/photons/model/PhotonsExperimentSceneModel.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/PhotonsExperimentSceneModel.ts b/js/photons/model/PhotonsExperimentSceneModel.ts
--- a/js/photons/model/PhotonsExperimentSceneModel.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/PhotonsExperimentSceneModel.ts	(date 1733342544685)
@@ -22,6 +22,7 @@
 import Photon, { PHOTON_SPEED } from './Photon.js';
 import PhotonDetector from './PhotonDetector.js';
 import { PhotonInteractionTestResult } from './PhotonsModel.js';
+import { PhotonSystem } from './PhotonSystem.js';
 import PolarizingBeamSplitter from './PolarizingBeamSplitter.js';
 import { TPhotonInteraction } from './TPhotonInteraction.js';
 
@@ -30,11 +31,6 @@
 };
 type PhotonsExperimentSceneModelOptions = SelfOptions & PickRequired<PhetioObjectOptions, 'tandem'>;
 
-// The number of photons created at construction time and pooled for usage in the experiment.  This value was
-// empirically determined to be sufficient to handle the maximum number of photons that could be active at any given
-// time, but may have to be adjusted if other aspects of the model are changed.
-const MAX_PHOTONS = 800;
-
 const PATH_LENGTH_UNIT = 0.2;
 
 export default class PhotonsExperimentSceneModel {
@@ -63,6 +59,7 @@
 
   // The photons that will be emitted and reflected in the experiment.
   public readonly photons: Photon[] = [];
+  public readonly photonSystem: PhotonSystem;
 
   // Whether the simulation is currently playing, which in this case means whether the photons are moving.
   public readonly isPlayingProperty: BooleanProperty;
@@ -173,10 +170,7 @@
 
     // Create all photons that will be used in the experiment.  It works better for phet-io if these are created at
     // construction time and activated and deactivated as needed, rather than creating and destroying them.
-    _.times( MAX_PHOTONS, index => {
-      const photon = new Photon( providedOptions.tandem.createTandem( `photon${index}` ) );
-      this.photons.push( photon );
-    } );
+    this.photonSystem = new PhotonSystem( providedOptions.tandem.createTandem( 'photonSystem' ) );
 
     // Create the Property that will be used to control whether the simulation is playing.
     this.isPlayingProperty = new BooleanProperty( true, {
@@ -246,7 +240,7 @@
               }
               else if ( interaction.interactionType === 'absorbed' ) {
                 // The photon was absorbed, so deactivate it.
-                photon.activeProperty.set( false );
+                photon.isActive = false;
                 photon.possibleStates.forEach( state => state.positionProperty.reset() );
               }
               else {
Index: js/photons/model/PhotonDetector.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/photons/model/PhotonDetector.ts b/js/photons/model/PhotonDetector.ts
--- a/js/photons/model/PhotonDetector.ts	(revision 8416d32bad7e5c9f6011e6791396a5987d9417d6)
+++ b/js/photons/model/PhotonDetector.ts	(date 1733338110576)
@@ -18,7 +18,7 @@
 import AveragingCounterNumberProperty from '../../common/model/AveragingCounterNumberProperty.js';
 import quantumMeasurement from '../../quantumMeasurement.js';
 import { PHOTON_BEAM_WIDTH } from './Laser.js';
-import Photon, { PhotonState } from './Photon.js';
+import Photon, { QuantumPossibleState } from './Photon.js';
 import { PhotonInteractionTestResult } from './PhotonsModel.js';
 import { TPhotonInteraction } from './TPhotonInteraction.js';
 
@@ -88,7 +88,7 @@
     } );
   }
 
-  public testForPhotonInteraction( photonState: PhotonState, photon: Photon, dt: number ): PhotonInteractionTestResult {
+  public testForPhotonInteraction( photonState: QuantumPossibleState, photon: Photon, dt: number ): PhotonInteractionTestResult {
 
     assert && assert( photon.activeProperty.value, 'save CPU cycles - don\'t use this method with inactive photons' );
 

AgustinVallejo added a commit that referenced this issue Dec 11, 2024
…alized, and rendered as Sprites, with Single and Continuous mode being handled in two different files.#66
@jbphet
Copy link
Collaborator Author

jbphet commented Dec 12, 2024

@AgustinVallejo and I have made the individual photons and particles into collections that are instrumented and serialized as a group rather than individually. This has greatly reduced the number of elements in the tree. Here's a screen shot from phet-io Studio running from main:

image

@AgustinVallejo - Shall we close this, or would you like me to review the implementation for screen 3 first?

@jbphet jbphet removed their assignment Dec 12, 2024
@AgustinVallejo
Copy link
Contributor

Let's review Screen 3 together today, and then close :) Thanks

@AgustinVallejo
Copy link
Contributor

Assigning back to @jbphet for closer review

@jbphet
Copy link
Collaborator Author

jbphet commented Jan 15, 2025

@AgustinVallejo - Code looks good. I made a number of changes to the documentation as part of this review, please have a look and make sure you're okay with these.

@jbphet jbphet assigned AgustinVallejo and unassigned jbphet Jan 15, 2025
@AgustinVallejo
Copy link
Contributor

Good changes, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants