diff --git a/doc/implementation-notes.md b/doc/implementation-notes.md index 45d00b0..a7e2b6c 100644 --- a/doc/implementation-notes.md +++ b/doc/implementation-notes.md @@ -23,6 +23,7 @@ to parameterize the model and view in quite a number of places. - Each of these experiments moves back and forth between preparation and measurement, starting in preparation. - In the docs, note the parallel between flipping the coin in the classical scene and re-preparing it in the quantum scene. +- Describe how the "seed" is used in two-state system to avoid having to send 10000 pieces of data. ### Photons Screen diff --git a/js/coins/model/CoinsExperimentSceneModel.ts b/js/coins/model/CoinsExperimentSceneModel.ts index 10593e6..250a6ab 100644 --- a/js/coins/model/CoinsExperimentSceneModel.ts +++ b/js/coins/model/CoinsExperimentSceneModel.ts @@ -134,13 +134,16 @@ export default class CoinsExperimentSceneModel extends PhetioObject { } else { - // The scene is moving from preparation mode to measurement mode. Force the coins to be in the initial state - // chosen by the user. - const singleCoinInitialState = this.systemType === 'classical' ? - this.initialCoinStateProperty.value as never : - null as never; - this.singleCoin.setMeasurementValueImmediate( singleCoinInitialState ); - this.coinSet.setMeasurementValuesImmediate( this.initialCoinStateProperty.value as QuantumCoinStates | ClassicalCoinStates ); + // The scene is moving from preparation mode to measurement mode. Set the coins to be in the initial state + // chosen by the user. If these are quantum coins and the initial state is set to superposed, set an arbitrary + // initial state, which is okay because the values won't be shown to the user. + let initialState = this.initialCoinStateProperty.value; + if ( this.systemType === 'quantum' && initialState === 'superposed' ) { + initialState = 'up'; + } + // TODO: How can this be better and avoid the "as never" weirdness? See https://github.com/phetsims/quantum-measurement/issues/42. + this.singleCoin.setMeasurementValuesImmediate( initialState as never ); + this.coinSet.setMeasurementValuesImmediate( initialState as never ); } } ); diff --git a/js/coins/view/CoinExperimentMeasurementArea.ts b/js/coins/view/CoinExperimentMeasurementArea.ts index 824c2a0..c8d55ec 100644 --- a/js/coins/view/CoinExperimentMeasurementArea.ts +++ b/js/coins/view/CoinExperimentMeasurementArea.ts @@ -94,7 +94,7 @@ export default class CoinExperimentMeasurementArea extends VBox { sceneModel.coinSet, sceneModel.coinSet.measurementStateProperty, sceneModel.coinSet.numberOfActiveSystemsProperty, - sceneModel.coinSet.measuredDataChanged, + sceneModel.coinSet.measuredDataChangedEmitter, { tandem: tandem.createTandem( 'multipleCoinTestBox' ) } ); const multiCoinExperimentHistogram = new CoinMeasurementHistogram( sceneModel.coinSet, sceneModel.systemType, { diff --git a/js/coins/view/MultiCoinTestBox.ts b/js/coins/view/MultiCoinTestBox.ts index 7d6ffd6..16f8db4 100644 --- a/js/coins/view/MultiCoinTestBox.ts +++ b/js/coins/view/MultiCoinTestBox.ts @@ -123,22 +123,22 @@ export default class MultiCoinTestBox extends HBox { coinNode.stopFlipping(); } - const state = coinSet.measuredValues[ index ]; - if ( state === null ) { - coinNode.displayModeProperty.value = 'masked'; - } - else if ( state === 'up' ) { + const measuredValue = coinSet.measuredValues[ index ]; + if ( measuredValue === 'up' ) { coinNode.displayModeProperty.value = 'up'; } - else if ( state === 'down' ) { + else if ( measuredValue === 'down' ) { coinNode.displayModeProperty.value = 'down'; } - else if ( state === 'heads' ) { + else if ( measuredValue === 'heads' ) { coinNode.displayModeProperty.value = 'heads'; } - else if ( state === 'tails' ) { + else if ( measuredValue === 'tails' ) { coinNode.displayModeProperty.value = 'tails'; } + else { + assert && assert( false, `unexpected measurement value: ${measuredValue} for index ${index}` ); + } } else if ( measurementState === 'preparingToBeMeasured' ) { @@ -150,7 +150,7 @@ export default class MultiCoinTestBox extends HBox { coinNode.startFlipping(); } } - else if ( measurementState === 'readyToBeMeasured' ) { + else if ( measurementState === 'readyToBeMeasured' || measurementState === 'measuredAndHidden' ) { if ( coinNode.isFlipping ) { coinNode.stopFlipping(); } diff --git a/js/common/model/TwoStateSystem.ts b/js/common/model/TwoStateSystem.ts index 3f45371..a82d728 100644 --- a/js/common/model/TwoStateSystem.ts +++ b/js/common/model/TwoStateSystem.ts @@ -42,18 +42,10 @@ export default class TwoStateSystem extends TwoStateSystemSet< } ); // Hook up to the data-changed emitter to update the data Property. - this.measuredDataChanged.addListener( () => { + this.measuredDataChangedEmitter.addListener( () => { this.measuredValueProperty.value = this.measuredValues[ 0 ]; } ); } - - /** - * Set the measurement value immediately for this system without transitioning through the 'preparingToBeMeasured' - * state. - */ - public setMeasurementValueImmediate( value: T ): void { - super.setMeasurementValuesImmediate( value ); - } } quantumMeasurement.register( 'TwoStateSystem', TwoStateSystem ); \ No newline at end of file diff --git a/js/common/model/TwoStateSystemSet.ts b/js/common/model/TwoStateSystemSet.ts index 15ca4dc..4b57824 100644 --- a/js/common/model/TwoStateSystemSet.ts +++ b/js/common/model/TwoStateSystemSet.ts @@ -23,7 +23,6 @@ import { SystemType } from './SystemType.js'; import Random from '../../../../dot/js/Random.js'; import TEmitter from '../../../../axon/js/TEmitter.js'; import Emitter from '../../../../axon/js/Emitter.js'; -import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js'; type SelfOptions = { systemType?: SystemType; @@ -65,15 +64,19 @@ export default class TwoStateSystemSet extends PhetioObject { // second, and 0.5 means no bias. public readonly biasProperty: NumberProperty; - // The seed that was used to generate the most recent set of measured values. This exists solely to support phet-io - - // it is conveyed in the state information and used to regenerate the data when phet-io state is set. This is done - // to avoid sending values for every individual measurement, which could be 10000 values. + // The seed that is used to generate the most recent set of measured values. This exists primarily as a way to + // support phet-io - it is conveyed in the state information and used to generate the data when phet-io state is set. + // This is done to avoid sending values for every individual measurement, which could be 10000 values. + // + // The contained value ranges from 0 to 1 inclusive, and 0 and 1 have special meaning - they indicate that all + // measurement values should be set to either the 0th or 1st valid value. All other values are used as a seed to a + // random number generator to produce random measurement values. public readonly seedProperty: NumberProperty; // An emitter that fires when the measured data changed, which is essentially any time a new measurement is made after // the system has been prepared for measurement. This is intended to be used as a signal to the view that and update // of the information being presented to the user is needed. - public readonly measuredDataChanged: TEmitter = new Emitter(); + public readonly measuredDataChangedEmitter: TEmitter = new Emitter(); public constructor( stateValues: readonly T[], initialState: T, @@ -92,7 +95,6 @@ export default class TwoStateSystemSet extends PhetioObject { super( options ); this.validValues = stateValues; - this.systemType = options.systemType; // TODO: This isn't how we should do this. See https://github.com/phetsims/quantum-measurement/issues/43. @@ -111,39 +113,37 @@ export default class TwoStateSystemSet extends PhetioObject { phetioReadOnly: true } ); - this.measuredValues = new Array( options.maxNumberOfSystems ); - _.times( options.maxNumberOfSystems, i => { - this.measuredValues[ i ] = initialState; - } ); - this.biasProperty = biasProperty; + this.measuredValues = new Array( options.maxNumberOfSystems ); - // Create the seed Property. - this.seedProperty = new NumberProperty( 1, { + // Create the seed Property. It's initial value is controlled by the specified initial value for measurements. + this.seedProperty = new NumberProperty( stateValues.indexOf( initialState ), { range: new Range( 0, 1 ), tandem: options.tandem.createTandem( 'seedProperty' ) } ); - // Monitor the seed for the random number generator. If this changes while setting phet-io state, the data will - // need to be updated. - this.seedProperty.lazyLink( seed => { - - if ( isSettingPhetioStateProperty.value && this.measurementStateProperty.value === 'revealed' ) { + // Monitor the seed that is used to create the measurement values. + this.seedProperty.link( seed => { - this.generateNewMeasurementValues(); + // Handle the "special case" values of 0 and 1, which sets all measurement values to one of the two valid values. + if ( seed === 0 || seed === 1 ) { + const valueToSet = stateValues[ seed ]; + _.times( this.numberOfActiveSystemsProperty.value, i => { + this.measuredValues[ i ] = valueToSet; + } ); + } + else { - // Create the measured values. + // Use the seed value to generate random values. const random = new Random( { seed: seed } ); _.times( this.numberOfActiveSystemsProperty.value, i => { - - // Only make a new measurement if one doesn't exist for this element. Otherwise, just keep the existing value. const valueSetIndex = random.nextDouble() < this.biasProperty.value ? 0 : 1; this.measuredValues[ i ] = this.validValues[ valueSetIndex ]; } ); - - // Signal that the data has been updated. - this.measuredDataChanged.emit(); } + + // Fire the emitter that signals a change to the data. + this.measuredDataChangedEmitter.emit(); } ); } @@ -176,7 +176,7 @@ export default class TwoStateSystemSet extends PhetioObject { if ( this.systemType === 'classical' ) { // Classical systems have deterministic values when measured. - this.generateNewMeasurementValues(); + this.generateNewRandomMeasurementValues(); this.measurementStateProperty.value = 'measuredAndHidden'; } else { @@ -201,7 +201,7 @@ export default class TwoStateSystemSet extends PhetioObject { if ( this.measurementStateProperty.value === 'readyToBeMeasured' ) { assert && assert( this.systemType === 'quantum', 'This point should only be reached for quantum systems' ); - this.generateNewMeasurementValues(); + this.generateNewRandomMeasurementValues(); } // Update the measurement state to indicate revealed. When this happens, the view should present the values to the @@ -237,7 +237,7 @@ export default class TwoStateSystemSet extends PhetioObject { // If the system is ready to be measured, but hasn't yet been, do it now. if ( this.measurementStateProperty.value === 'readyToBeMeasured' ) { - this.generateNewMeasurementValues(); + this.generateNewRandomMeasurementValues(); // Change the state to represent that this system has now been measured. this.measurementStateProperty.value = 'revealed'; @@ -249,40 +249,45 @@ export default class TwoStateSystemSet extends PhetioObject { }; } - private generateNewMeasurementValues(): void { - - // Generate a new seed that will subsequently be used to generate the random data. - this.seedProperty.value = dotRandom.nextDouble(); - - // Create the measured values. - const random = new Random( { seed: this.seedProperty.value } ); - _.times( this.numberOfActiveSystemsProperty.value, i => { - const valueSetIndex = random.nextDouble() < this.biasProperty.value ? 0 : 1; - this.measuredValues[ i ] = this.validValues[ valueSetIndex ]; - } ); - - // Signal that the data has been updated. - this.measuredDataChanged.emit(); + /** + * Generate new random measured values for this system. + */ + private generateNewRandomMeasurementValues(): void { + + // New values are generated by create a new random seed and setting it. The listener for the seedProperty does the + // actual generation. Note that the value of 0 is not allowed because it has special significance. + let newSeed; + do { + newSeed = dotRandom.nextDouble(); + } while ( newSeed === 0 ); + this.seedProperty.value = newSeed; } /** - * Set the measurement value immediately for all elements in this set without transitioning through the - * 'preparingToBeMeasured' state. + * Set the measurement values immediately to the provided value for all elements in this set without transitioning + * through the 'preparingToBeMeasured' state. */ public setMeasurementValuesImmediate( value: T ): void { + + // Cancel any in-progress preparation. if ( this.preparingToBeMeasuredTimeoutListener ) { stepTimer.clearTimeout( this.preparingToBeMeasuredTimeoutListener ); this.preparingToBeMeasuredTimeoutListener = null; } - _.times( this.numberOfActiveSystemsProperty.value, i => { - this.measuredValues[ i ] = value; - } ); + + // Set the seed value to the value that will incite its listener to update the data to the provided value. + const valueIndex = this.validValues.indexOf( value ); + assert && assert( valueIndex === 0 || valueIndex === 1 ); + this.seedProperty.value = valueIndex; + + // Update the measurement state. this.measurementStateProperty.value = this.systemType === 'classical' ? 'measuredAndHidden' : 'readyToBeMeasured'; } public reset(): void { this.measurementStateProperty.reset(); this.numberOfActiveSystemsProperty.reset(); + this.seedProperty.reset(); } }