phetsims / axon

Axon provides powerful and concise models for interactive simulations, based on observable Properties and related patterns.
MIT License
10 stars 8 forks source link

Always include all PhET-iO state keys for Property types #357

Closed zepumph closed 3 years ago

zepumph commented 3 years ago

From https://github.com/phetsims/phet-io/issues/1774. In order to have a stateSchema based on each IOType, we need to have a consistent state for each IOType. This means that we can no longer conditionally add state keys like units, range, etc. Tagging @samreid because I feel like doing this as a side issue, before continuing with https://github.com/phetsims/phet-io/issues/1774, and I have hope that it won't interfere too badly with the patch over in that issue.

zepumph commented 3 years ago

I am finding that this is increasing the number of lines in the API file by ~7-10%. I don't think that is a deal breaker, but wanted to note that I specifically removed this comment in Property about units:

// Only supply units if they were specified, to avoid seeing "units: null" in so many properties, see https://github.com/phetsims/phet-io/issues/1315

Looks like that issue has reminded to me to look into studio to see if any of these changes will harm the wrapper side.

Here is my current changeset:

Index: axon/js/NumberProperty.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/axon/js/NumberProperty.js b/axon/js/NumberProperty.js
--- a/axon/js/NumberProperty.js (revision d848101bd6e9f29d49173b5f41e8a5c7baa70516)
+++ b/axon/js/NumberProperty.js (date 1621720087193)
@@ -240,19 +240,14 @@
     const parentStateObject = PropertyIOImpl.toStateObject( numberProperty );

     // conditionals to avoid keys with value "null" in state objects
-    if ( numberProperty.numberType ) {
-      parentStateObject.numberType = numberProperty.numberType;
-    }
+    parentStateObject.numberType = NullableIO( StringIO ).toStateObject( numberProperty.numberType );
+
+    parentStateObject.range = NullableIO( Range.RangeIO ).toStateObject( numberProperty.rangeProperty.value );

-    if ( numberProperty.rangeProperty.value ) {
-      parentStateObject.range = Range.RangeIO.toStateObject( numberProperty.rangeProperty.value );
-      if ( numberProperty.rangeProperty.isPhetioInstrumented() ) {
-        parentStateObject.rangePhetioID = StringIO.toStateObject( numberProperty.rangeProperty.tandem.phetioID );
-      }
-    }
-    if ( numberProperty.step ) {
-      parentStateObject.step = NumberIO.toStateObject( numberProperty.step );
-    }
+    const hasRangePhetioID = numberProperty.rangeProperty && numberProperty.rangeProperty.isPhetioInstrumented();
+    parentStateObject.rangePhetioID = hasRangePhetioID ? StringIO.toStateObject( numberProperty.rangeProperty.tandem.phetioID ) : null;
+
+    parentStateObject.step = NullableIO( NumberIO ).toStateObject( numberProperty.step );
     return parentStateObject;
   },
   applyState: ( numberProperty, stateObject ) => {
@@ -260,6 +255,7 @@
     PropertyIOImpl.applyState( numberProperty, stateObject );
     numberProperty.step = stateObject.step;
     numberProperty.numberType = stateObject.numberType;
+    // TODO: apply range if rangePhetioID is null?
   }
 } );

Index: scenery/js/nodes/IndexedNodeIO.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/nodes/IndexedNodeIO.js b/scenery/js/nodes/IndexedNodeIO.js
--- a/scenery/js/nodes/IndexedNodeIO.js (revision 6734e11f7f1178eebeb2250aecd5601a8dc2ffb6)
+++ b/scenery/js/nodes/IndexedNodeIO.js (date 1621720534436)
@@ -30,12 +30,15 @@
       assert && assert( node.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' );
       stateObject.index = node.parents[ 0 ].indexOfChild( node );
     }
+    else {
+      stateObject.index = null;
+    }
     return stateObject;
   },
-  applyState: ( node, fromStateObject ) => {
-    if ( node.parents[ 0 ] ) {
+  applyState: ( node, stateObject ) => {
+    if ( node.parents[ 0 ] && stateObject.index ) {
       assert && assert( node.parents.length === 1, 'IndexedNodeIO only supports nodes with a single parent' );
-      node.parents[ 0 ].moveChildToIndex( node, fromStateObject.index );
+      node.parents[ 0 ].moveChildToIndex( node, stateObject.index );
     }
   },
   methods: {
Index: axon/js/Property.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/axon/js/Property.js b/axon/js/Property.js
--- a/axon/js/Property.js   (revision d848101bd6e9f29d49173b5f41e8a5c7baa70516)
+++ b/axon/js/Property.js   (date 1621720310656)
@@ -13,6 +13,7 @@
 import FunctionIO from '../../tandem/js/types/FunctionIO.js';
 import IOType from '../../tandem/js/types/IOType.js';
 import NullableIO from '../../tandem/js/types/NullableIO.js';
+import StringIO from '../../tandem/js/types/StringIO.js';
 import VoidIO from '../../tandem/js/types/VoidIO.js';
 import axon from './axon.js';
 import Multilink from './Multilink.js';
@@ -579,7 +580,7 @@
       events: [ 'changed' ],
       parameterTypes: [ parameterType ],
       toStateObject: property => {
-        assert && assert( parameterType.toStateObject, `toStateObject doesnt exist for ${parameterType.typeName}` );
+        assert && assert( parameterType.toStateObject, `toStateObject doesn't exist for ${parameterType.typeName}` );
         const stateObject = {
           value: parameterType.toStateObject( property.value )
         };
@@ -590,15 +591,15 @@
             return parameterType.toStateObject( v );
           } );
         }
-
-        // Only supply units if they were specified, to avoid seeing "units: null" in so many properties, see https://github.com/phetsims/phet-io/issues/1315
-        if ( property.units ) {
-          stateObject.units = property.units;
+        else {
+          stateObject.validValues = null;
         }
+
+        stateObject.units = NullableIO( StringIO ).toStateObject( property.units );
         return stateObject;
       },
       applyState: ( property, stateObject ) => {
-        property.units = stateObject.units;
+        property.units = NullableIO( StringIO ).fromStateObject( stateObject.units );
         property.set( parameterType.fromStateObject( stateObject.value ) );

         if ( stateObject.validValues ) {
Index: scenery/js/input/SceneryEvent.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery/js/input/SceneryEvent.js b/scenery/js/input/SceneryEvent.js
--- a/scenery/js/input/SceneryEvent.js  (revision 6734e11f7f1178eebeb2250aecd5601a8dc2ffb6)
+++ b/scenery/js/input/SceneryEvent.js  (date 1621720534436)
@@ -133,12 +133,8 @@
       type: event.type
     };

-    if ( event.domEvent ) {
-      eventObject.domEventType = event.domEvent.type;
-    }
-    if ( event.pointer && event.pointer.point ) {
-      eventObject.point = Vector2.Vector2IO.toStateObject( event.pointer.point );
-    }
+    eventObject.domEventType = event.domEvent ? event.domEvent.type : null;
+    eventObject.point = ( event.pointer && event.pointer.point ) ? Vector2.Vector2IO.toStateObject( event.pointer.point ) : null;

     // Note: If changing the contents of this object, please document it in the public documentation string.
     return eventObject;
zepumph commented 3 years ago

Committed above. @samreid I don't love the increased file size, just because it is total fluff, but I do think we should proceed with https://github.com/phetsims/phet-io/issues/1774 before reflecting on ways to improve the number of lines of data in the api file that are currently just storing "null". Pease review.

samreid commented 3 years ago

For energy-forms-and-changes, it takes the API file size from 25695 lines to 28574 lines, an 11% increase. I reviewed all the commits and it seems good, I don't have any recommendations. I think this issue can be closed, but running it past @zepumph in case he has other ideas.

samreid commented 3 years ago

CT is showing this issue:

arithmetic : phet-io-wrappers-tests : assert
https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/phet-io-wrappers/phet-io-wrappers-tests.html?sim=arithmetic&phetioDebug
11 out of 12 tests passed. 1 failed.
SimTests: arithmetic: iframe api failed:
Uncaught Error: Assertion failed: unexpected parameter to toStateObject: value failed isValidValue: undefined
Error: Assertion failed: unexpected parameter to toStateObject: value failed isValidValue: undefined
at window.assertions.assertFunction (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/assert/js/assert.js:25:13)
at Object.isValueValid (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/axon/js/ValidatorDef.js:324:39)
at validate (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/axon/js/validate.js:36:18)
at IOType.toStateObject (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/tandem/js/types/IOType.js:147:7)
at Object.toStateObject (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/axon/js/Property.js:598:52)
at IOType.toStateObject (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/tandem/js/types/IOType.js:148:21)
at PhetioStateEngine.getValueJSON (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/phet-io/js/PhetioStateEngine.js:107:49)
at https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/phet-io/js/PhetioStateEngine.js:166:34
at Array.forEach (<anonymous>)
at PhetioStateEngine.getState (https://bayes.colorado.edu/continuous-testing/ct-snapshots/1621861494146/phet-io/js/PhetioStateEngine.js:162:38)

It is coming from lines like these:

stateObject.units = NullableIO( StringIO ).toStateObject( property.units );

I asked in slack:

Nullable doesn’t support property.units being undefined. Should I make it so that Property.units is null when not specified?

@jonathanolson added a 👍 and @zepumph replied:

That seems to be the way we are heading in PhET-iO. Consistent schema is something we have been lacking a bit since javascript lets up.

Self-assigning for investigation.

zepumph commented 3 years ago

I don't immediately understand this error, because I see this line:

https://github.com/phetsims/axon/blob/0bb2b45ad2930b89e2b4c4706313876e480cd360/js/Property.js#L81-L83

That said, when running this line with the chrome tools:

http://localhost:8080/phet-io-wrappers/phet-io-wrappers-tests.html?sim=ph-scale&phetioDebug&debugger

I immediately pause in the buggy case for a BooleanProperty called "phScale.general.model.activeProperty".

zepumph commented 3 years ago

I'm working more on this now.

zepumph commented 3 years ago

Looks like I had written a unit test that was assuming that the state of a BooleanProperty was: { value: true }, since that instance had no units or validValues. Once adding all values as null, we ran into trouble when trying to call PropertyIO.applyState on it.

This was a bad assertion error because it was very downstream of the problem.

  1. Try to set state with a state object that was incomplete
  2. Property.applyState then sets non-existent values in the state object to the Property instance (so property.units is now undefined) WE SHOULD ERROR HERE WHEN THE STATE OBJECT COMING IN IS WRONG.
  3. Then later on PropertyIO.toStateObject is called, and it can't serialize property.units because it is undefined, which is not supported by its IOType. WE DO ERROR HERE.

Over in https://github.com/phetsims/phet-io/issues/1774, I think we should add a step to applyState that validates the stateObject before it is passed into applyState. I'll note that over there.

zepumph commented 3 years ago

Back to @samreid for review

I think this issue can be closed, but running it past @zepumph in case he has other ideas.

Feel free to close if you have nothing else here.

zepumph commented 3 years ago

I went over this with @samreid, we will continue with fixes in the stateSChema issue. Closing