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

In mirror inputs wrapper, top particles different from bottom particles #325

Open
Tracked by #348
liammulh opened this issue Aug 11, 2020 · 17 comments
Open
Tracked by #348

Comments

@liammulh
Copy link
Member

Test Device

MacBook Air

Operating System

macOS 10.15.5

Browser

Safari 13.1.1

Problem Description

For phetsims/qa#525. This bug seems to be platform-specific. That is, I don't see this bug on macOS + Firefox or macOS + Chrome. In the mirror inputs wrapper, the particles in the top iframe have different positions/velocities from the particles in the bottom iframe. Easy to reproduce; just mess around with the heater in the states screen. The two iframes should diverge quickly.

Visuals

See, I'm not crazy:
nani

@jbphet
Copy link
Contributor

jbphet commented Aug 14, 2020

I was able to duplicate this on my machine, in Chrome, using the 1.2.0-rc.1 release, through the following fairly simply process:

  • load the mirror wrapper
  • select the "States" screen
  • press the "Liquid" button to set the state to liquid (leave the substance set to neon)
  • wait for several minutes

The two start off in sync, but then diverge over time. Here is a screenshot:

image

There must still be some usages of unseeded random number generation in the code, or some sensitive dependence on initial conditions going on.

@jbphet
Copy link
Contributor

jbphet commented Aug 14, 2020

I reviewed this with @kathy-phet and @arouinfar, and they said that the customer for whom the 1.2 release is being created is not planning to use input event mirroring, at least not that they know of. Therefore, this is not blocking for the 1.2 release.

I'm not sure how important this is in the phet-io scheme of things, and I'm not sure who to even ask. I'm going to mark this for the phet-io meeting and perhaps we can collectively decide whether this is something I, or someone else, should spend time on.

@jbphet jbphet removed their assignment Aug 14, 2020
@KatieWoe
Copy link
Contributor

In phetsims/qa#531, noting that this can lead to a larger problem.
If the lid on the second screen comes off, different amounts of particles can escape. This means that putting the lid on and causing a second explosion will lead to different conditions and one "blowing up" first. This is fairly easy to recreate, and may even be possible on the first explosion.
Leadstoaproblem

@kathy-phet
Copy link

Good to know, and to discuss. Still not blocking for publication at this time.

@zepumph
Copy link
Member

zepumph commented Aug 20, 2020

@jbphet the primary reason for this kind of bug is that something in the sim using random is not tied to phet.joist.random. (which is seeded to be the same in both sims). Since this is platform specific, it feels less likely that this is the exact problem.

@jbphet said:

I was able to duplicate this on my machine, in Chrome, using the 1.2.0-rc.1 release, through the following fairly simply process:

I think that he has a windows machine, and I was able to reproduce on mine using his steps. Thanks for the guide!

Looking through the SOM repo, I see that all usages of random are using phet.joist.random. @jbphet you know the sim better than I, can you think of any non-reproducible parts of the particle model that may be causing this deviation? Likely not since this sim has been code-reviewed, but it is my primary thought on the cause of this bug.

@zepumph
Copy link
Member

zepumph commented Aug 20, 2020

@samreid do you have any other ideas on why these may be getting off?

@zepumph
Copy link
Member

zepumph commented Aug 20, 2020

I'm going to mark this for the phet-io meeting and perhaps we can collectively decide whether this is something I, or someone else, should spend time on.

This feels like an important bug to me. Without reproducible playbacks we lose a large feature of this sim, and publishing it without this feature will not be tracked (see https://github.com/phetsims/phet-io/issues/1609) and it is likely that we will forget about this, and potentially try to use this for a data study in the future.

@samreid
Copy link
Member

samreid commented Aug 20, 2020

At today's meeting, @kathy-phet and @jbphet indicated this may need to be fixed after the initial 1.0 release.

@samreid
Copy link
Member

samreid commented Aug 21, 2020

  • I tried setting up a unit test, creating two models with identical Random seeds and running them side-by-side, and that gave the same results.
  • I tried setting the frame sizes to be the same width, and saw the same problem
  • I printed out the dts and found the downstream sim is running a few dts before it starts processing the upstream dts. This could be throwing it out of whack.
  • I tried starting the model paused, and that seemed to fix the problem. UPDATE: no, this has the same problem
  • I used a guard to make sure the downstream didn't have an extra dt in the model, but that still had the problem.

@kathy-phet
Copy link

kathy-phet commented Aug 21, 2020 via email

@samreid
Copy link
Member

samreid commented Aug 21, 2020

This patch appears to have reproducible playbacks, but starts the model paused:

Index: js/common/model/MultipleParticleModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/common/model/MultipleParticleModel.js	(revision ba4746a10c695b35de1cf3a0bcc77db642b20fd4)
+++ js/common/model/MultipleParticleModel.js	(date 1597983534925)
@@ -26,6 +26,7 @@
 import EnumerationProperty from '../../../../axon/js/EnumerationProperty.js';
 import NumberProperty from '../../../../axon/js/NumberProperty.js';
 import ObservableArray from '../../../../axon/js/ObservableArray.js';
+import Random from '../../../../dot/js/Random.js';
 import Range from '../../../../dot/js/Range.js';
 import Utils from '../../../../dot/js/Utils.js';
 import Vector2 from '../../../../dot/js/Vector2.js';
@@ -133,6 +134,8 @@
       tandem: tandem,
       phetioType: MultipleParticleModelIO
     } );
+    // this.random = new Random( { seed: 123 } );
+    this.random = phet.joist.random;
 
     //-----------------------------------------------------------------------------------------------------------------
     // observable model properties
@@ -176,7 +179,7 @@
     } );
 
     // @public (read-write)
-    this.isPlayingProperty = new BooleanProperty( true, {
+    this.isPlayingProperty = new BooleanProperty( false, {
       tandem: tandem.createTandem( 'isPlayingProperty' )
     } );
 
@@ -647,15 +650,16 @@
       'injection attempted when there is no room in the data set'
     );
 
+    const random = this.random;
     // Choose an injection angle with some amount of randomness.
-    const injectionAngle = ( phet.joist.random.nextDouble() - 0.5 ) * INJECTED_MOLECULE_ANGLE_SPREAD;
+    const injectionAngle = ( random.nextDouble() - 0.5 ) * INJECTED_MOLECULE_ANGLE_SPREAD;
 
     // Set the molecule's velocity.
     const xVel = Math.cos( injectionAngle ) * INJECTED_MOLECULE_SPEED;
     const yVel = Math.sin( injectionAngle ) * INJECTED_MOLECULE_SPEED;
 
     // Set the rotational velocity to a random value within a range (will be ignored for single atom cases).
-    const moleculeRotationRate = ( phet.joist.random.nextDouble() - 0.5 ) * ( Math.PI / 4 );
+    const moleculeRotationRate = ( random.nextDouble() - 0.5 ) * ( Math.PI / 4 );
 
     // Set the position(s) of the atom(s).
     const atomsPerMolecule = this.moleculeDataSet.atomsPerMolecule;
@@ -679,7 +683,7 @@
 
       // randomize the rotational angle of multi-atom molecules
       this.moleculeDataSet.moleculeRotationAngles[ this.moleculeDataSet.getNumberOfMolecules() - 1 ] =
-        phet.joist.random.nextDouble() * 2 * Math.PI;
+        random.nextDouble() * 2 * Math.PI;
     }
 
     // Position the atoms that comprise the molecules.
@@ -726,7 +730,7 @@
         case SubstanceType.WATER:
           this.scaledAtoms.push( new ScaledAtom( AtomType.OXYGEN, 0, 0 ) );
           this.scaledAtoms.push( new HydrogenAtom( 0, 0, true ) );
-          this.scaledAtoms.push( new HydrogenAtom( 0, 0, phet.joist.random.nextDouble() > 0.5 ) );
+          this.scaledAtoms.push( new HydrogenAtom( 0, 0, this.random.nextDouble() > 0.5 ) );
           break;
 
         default:
@@ -827,6 +831,7 @@
    * @public
    */
   stepInTime( dt ) {
+    console.log( window.phet.chipper.queryParameters.frameTitle + ', dt = ' + dt );
 
     this.moleculeInjectedThisStep = false;
 
@@ -1089,7 +1094,7 @@
     this.atomPositionUpdater = DiatomicAtomPositionUpdater;
     this.moleculeForceAndMotionCalculator = new DiatomicVerletAlgorithm( this );
     this.isoKineticThermostat = new IsokineticThermostat( this.moleculeDataSet, this.minModelTemperature );
-    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature );
+    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature, this.random );
 
     const numberOfMolecules = numberOfAtoms / 2;
     const atomPositionInVector = new Vector2( 0, 0 );
@@ -1145,7 +1150,7 @@
     this.atomPositionUpdater = WaterAtomPositionUpdater;
     this.moleculeForceAndMotionCalculator = new WaterVerletAlgorithm( this );
     this.isoKineticThermostat = new IsokineticThermostat( this.moleculeDataSet, this.minModelTemperature );
-    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature );
+    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature, this.random );
 
     // Create the individual atoms and add them to the data set.
     const atomPositionInVector = new Vector2( 0, 0 );
@@ -1222,7 +1227,7 @@
     this.atomPositionUpdater = MonatomicAtomPositionUpdater;
     this.moleculeForceAndMotionCalculator = new MonatomicVerletAlgorithm( this );
     this.isoKineticThermostat = new IsokineticThermostat( this.moleculeDataSet, this.minModelTemperature );
-    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature );
+    this.andersenThermostat = new AndersenThermostat( this.moleculeDataSet, this.minModelTemperature, this.random );
 
     // Create the individual atoms and add them to the data set.
     const atomPositions = [];
Index: js/common/model/engine/MonatomicPhaseStateChanger.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/common/model/engine/MonatomicPhaseStateChanger.js	(revision ba4746a10c695b35de1cf3a0bcc77db642b20fd4)
+++ js/common/model/engine/MonatomicPhaseStateChanger.js	(date 1597983118654)
@@ -25,7 +25,7 @@
   constructor( multipleParticleModel ) {
     super( multipleParticleModel );
     this.positionUpdater = MonatomicAtomPositionUpdater;
-    this.random = phet.joist.random;
+    this.random = multipleParticleModel.random;
   }
 
   /**
Index: js/common/model/engine/kinetic/AndersenThermostat.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/common/model/engine/kinetic/AndersenThermostat.js	(revision ba4746a10c695b35de1cf3a0bcc77db642b20fd4)
+++ js/common/model/engine/kinetic/AndersenThermostat.js	(date 1597982494964)
@@ -25,7 +25,7 @@
    * @param {MoleculeForceAndMotionDataSet} moleculeDataSet  - Data set on which operations will be performed.
    * @param {number} minTemperature  - The temperature that should be considered absolute zero, below which motion should cease
    */
-  constructor( moleculeDataSet, minTemperature ) {
+  constructor( moleculeDataSet, minTemperature,random ) {
 
     // @public target temperature in normalized model units
     this.targetTemperature = SOMConstants.INITIAL_TEMPERATURE;
@@ -37,7 +37,7 @@
     this.moleculeDataSet = moleculeDataSet;
 
     // @private - pseudo-random number generator
-    this.random = phet.joist.random;
+    this.random = random;
 
     // @private {Vector2} - reusable vector used for calculating velocity changes
     this.previousParticleVelocity = new Vector2( 0, 0 );
Index: js/common/model/engine/AbstractPhaseStateChanger.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/common/model/engine/AbstractPhaseStateChanger.js	(revision ba4746a10c695b35de1cf3a0bcc77db642b20fd4)
+++ js/common/model/engine/AbstractPhaseStateChanger.js	(date 1597982494968)
@@ -29,7 +29,7 @@
     // @private
     this.multipleParticleModel = multipleParticleModel;
     this.moleculePosition = new Vector2( 0, 0 );
-    this.random = phet.joist.random;
+    this.random = multipleParticleModel.random;
     this.reusableVector = new Vector2( 0, 0 );
   }
 

@samreid
Copy link
Member

samreid commented Aug 21, 2020

This patch shows that the random number generator is being called a different number of times in the upstream and downstream simulations:

Index: js/Random.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/Random.js	(revision 60312e5cc6023257d4734a61001edbd439685fc2)
+++ js/Random.js	(date 1597983759969)
@@ -135,6 +135,10 @@
    * @returns {number} - the random number
    */
   nextDouble() {
+
+    window.randCount = window.randCount || 0;
+    window.randCount++;
+    console.log( phet.preloads.phetio.queryParameters.frameTitle, window.randCount );
     return this.seed === null ? Math.random() : this.seedrandom(); // eslint-disable-line bad-sim-text
   }
 

@samreid
Copy link
Member

samreid commented Aug 21, 2020

Using the above patch that counts random calls, I confirmed that putting isPlayingProperty to default to true causes the number of calls to the random sequence to line up.

I should also mention many sightings of where the destination value prints out before the source value does:

image

@samreid
Copy link
Member

samreid commented Aug 21, 2020

This patch appears to resolve the problem, but I am confused why, especially after viewing the list of listeners in the timer. It's at a good point to check in with or collaborate with @zepumph.

Index: js/Sim.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/Sim.js	(revision e10aa8a8701e863ac52d050e6c3071c802f65d10)
+++ js/Sim.js	(date 1597986974133)
@@ -879,9 +879,7 @@
               // If the sim is in playback mode, then run a single frame. This makes sure that anything kicked to the next
               // frame with timer.runOnNextFrame during startup can clear out before beginning playback.
               if ( phet.joist.playbackModeEnabledProperty.value ) {
-
-                // Mark this as a "root" event so that events caused by this don't duplicate in the playback
-                self.stepOneFrame();
+                timer.emit( 0 );
               }
 
               // After the application is ready to go, remove the splash screen and progress bar.  Note the splash

@samreid samreid assigned zepumph and unassigned samreid Aug 21, 2020
@zepumph
Copy link
Member

zepumph commented Aug 21, 2020

We are close to a commit! I have to run though:

Index: dot/js/Random.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- dot/js/Random.js	(revision 60312e5cc6023257d4734a61001edbd439685fc2)
+++ dot/js/Random.js	(date 1598028641316)
@@ -46,6 +46,11 @@
     // Math.seedrandom is provided by seedrandom.js, see https://github.com/davidbau/seedrandom.
     // @private {function:number|null} initialized via setSeed below
     this.seedrandom = ( this.seed !== null ) ? new Math.seedrandom( this.seed + '' ) : null;
+
+    // @public (read-only)
+    this.randCount = 0;
+
+    Random.allRandomInstances.push( this );
   }
 
   /**
@@ -135,6 +140,10 @@
    * @returns {number} - the random number
    */
   nextDouble() {
+    if( window.blarg){
+      debugger;
+    }
+    this.randCount++;
     return this.seed === null ? Math.random() : this.seedrandom(); // eslint-disable-line bad-sim-text
   }
 
@@ -182,6 +191,8 @@
   }
 }
 
+Random.allRandomInstances = [];
+
 dot.register( 'Random', Random );
 
 export default Random;
\ No newline at end of file
Index: phet-io-wrappers/mirror-inputs/mirrorInputs.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- phet-io-wrappers/mirror-inputs/mirrorInputs.js	(revision 3e8368293628108337895ec1b3d8e011bbc8b7ff)
+++ phet-io-wrappers/mirror-inputs/mirrorInputs.js	(date 1598027457495)
@@ -44,6 +44,14 @@
   // Clickable only once initialized, see https://github.com/phetsims/phet-io-wrappers/issues/359
   sourceClient.addSimInitializedListener( () => {
     sourceFrame.classList.remove( 'unclickable' );
+
+    assert && setTimeout( () => {
+      sourceClient.invoke( 'phetioEngine', 'getRandomSeed', [], sourceRandomSeed => {
+        destinationClient.invoke( 'phetioEngine', 'getRandomSeed', [], destinationRandomSeed => {
+          assert && assert( sourceRandomSeed === destinationRandomSeed, 'random seeds must match' );
+        } );
+      } );
+    }, 10 );
   } );
 
   // Cannot use phetioClient.launchSim because we need to launch sim 1 to get its seed, then launch sim 2 to set the seed, and
Index: joist/js/Sim.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- joist/js/Sim.js	(revision e10aa8a8701e863ac52d050e6c3071c802f65d10)
+++ joist/js/Sim.js	(date 1598030075100)
@@ -22,6 +22,7 @@
 import timer from '../../axon/js/timer.js';
 import Bounds2 from '../../dot/js/Bounds2.js';
 import Dimension2 from '../../dot/js/Dimension2.js';
+import Random from '../../dot/js/Random.js';
 import DotUtils from '../../dot/js/Utils.js';
 import inherit from '../../phet-core/js/inherit.js';
 import merge from '../../phet-core/js/merge.js';
@@ -42,16 +43,16 @@
 import Heartbeat from './Heartbeat.js';
 import HomeScreen from './HomeScreen.js';
 import HomeScreenView from './HomeScreenView.js';
+import joist from './joist.js';
+import joistStrings from './joistStrings.js';
 import LookAndFeel from './LookAndFeel.js';
 import MemoryMonitor from './MemoryMonitor.js';
 import NavigationBar from './NavigationBar.js';
+import packageJSON from './packageJSON.js';
 import Profiler from './Profiler.js';
 import QueryParametersWarningDialog from './QueryParametersWarningDialog.js';
 import ScreenIO from './ScreenIO.js';
 import ScreenshotGenerator from './ScreenshotGenerator.js';
-import joist from './joist.js';
-import joistStrings from './joistStrings.js';
-import packageJSON from './packageJSON.js';
 import selectScreens from './selectScreens.js';
 import LegendsOfLearningSupport from './thirdPartySupport/LegendsOfLearningSupport.js';
 import updateCheck from './updateCheck.js';
@@ -876,12 +877,15 @@
               // Schedules animation updates and runs the first step()
               self.boundRunAnimationLoop();
 
-              // If the sim is in playback mode, then run a single frame. This makes sure that anything kicked to the next
-              // frame with timer.runOnNextFrame during startup can clear out before beginning playback.
+              // If the sim is in playback mode, then flush the timer's listeners. This makes sure that anything kicked
+              // to the next frame with `timer.runOnNextFrame` during startup (like every notification about a PhET-iO
+              // instrumented element in phetioEngine.phetioObjectAdded()) can clear out before beginning playback.
               if ( phet.joist.playbackModeEnabledProperty.value ) {
-
-                // Mark this as a "root" event so that events caused by this don't duplicate in the playback
-                self.stepOneFrame();
+                const beforeCounts = Random.allRandomInstances.map( n => n.randCount );
+                self.timer.emit( 0 );
+                const afterCounts = Random.allRandomInstances.map( n => n.randCount );
+                assert && assert( _.isEqual( beforeCounts, afterCounts ),
+                  `Random was called more times in the playback sim on startup, before: ${beforeCounts}, after: ${afterCounts}` );
               }
 
               // After the application is ready to go, remove the splash screen and progress bar.  Note the splash
Index: joist/js/Heartbeat.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- joist/js/Heartbeat.js	(revision e10aa8a8701e863ac52d050e6c3071c802f65d10)
+++ joist/js/Heartbeat.js	(date 1598030183256)
@@ -13,6 +13,9 @@
 // variables
 let started = false;
 
+// a unique number to make sure safari doesn't get sleepy, see usage.
+let counter = 0;
+
 const Heartbeat = {
 
   /**
@@ -41,7 +44,7 @@
     // prevent Safari from going to sleep, see https://github.com/phetsims/joist/issues/140
     sim.frameStartedEmitter.addListener( function() {
       if ( sim.frameCounter % 1000 === 0 ) {
-        heartbeatDiv.innerHTML = phet.joist.random.nextDouble();
+        heartbeatDiv.innerHTML = ++counter;
       }
     } );
   }

chrisklus added a commit to phetsims/joist that referenced this issue Aug 21, 2020
@chrisklus
Copy link
Contributor

@samreid @zepumph and I worked on this. We ended up finding the source of the mismatched number of calls to Random.nextDouble() - it was the downstream sim using its first random number for Heartbeat.js, which was slightly earlier than the upstream was. This causes the sims to start using random number that were misaligned by one. To fix, we used a different method for writing unique content every 1000 frames, which resulted in a matching number of calls to Random in both sims. We also are now tracking the number of random calls from both sims and are asserting that they stay in sync.

Regardless, we kept the proposed fix in from #325 (comment) since it seemed safer.

@jbphet the following commits are ready to be cherry-picked:

phetsims/joist@1ffb7b6
phetsims/joist@1bf6490
phetsims/joist@a6d1918
https://github.com/phetsims/phet-io-wrappers/commit/c7742c62cc742fb2531d8c001cf2d22b4bd688ed
phetsims/dot@9800d97

@jbphet
Copy link
Contributor

jbphet commented Aug 21, 2020

Unassigning until the maintenance release is designated as one of my priorities.

@jbphet jbphet removed their assignment Aug 21, 2020
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

7 participants