phetsims / tandem

Simulation-side code for PhET-iO
MIT License
0 stars 5 forks source link

Create ReferenceDataTypeIO #292

Closed zepumph closed 1 year ago

zepumph commented 1 year ago

Over in https://github.com/phetsims/studio/issues/291, @pixelzoom and @samreid and I realized that we need to support serializing arrays and then deserializing them back to their exact array references. This patch helps get us close, but @samreid will need to take some time to clean it up before commit, but we feel confident that it will fix the graphSetProperty serialization that is blocking Calculus Grapher.

```diff Subject: [PATCH] update doc and add BLL to api-stable, https://github.com/phetsims/beers-law-lab/issues/315 --- Index: axon/js/ValueComparison.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/axon/js/ValueComparison.ts b/axon/js/ValueComparison.ts new file mode 100644 --- /dev/null (date 1675371630900) +++ b/axon/js/ValueComparison.ts (date 1675371630900) @@ -0,0 +1,61 @@ +// Copyright 2019-2023, University of Colorado Boulder + +/** + * The definition file for "validators" used to validate values. This file holds associated logic that validates the + * schema of the "validator" object, as well as testing if a value adheres to the restrictions provided by a validator. + * See validate.js for usage with assertions to check that values are valid. + * + * Examples: + * + * A Validator that only accepts number values: + * { valueType: 'number' } + * + * A Validator that only accepts the numbers "2" or "3": + * { valueType: 'number', validValues: [ 2, 3 ] } + * + * A Validator that accepts any Object: + * { valueType: Object } + * + * A Validator that accepts EnumerationDeprecated values (NOTE! This is deprecated, use the new class-based enumeration pattern as the valueType): + * { valueType: MyEnumeration } + * and/or + * { validValues: MyEnumeration.VALUES } + * + * A Validator that accepts a string or a number greater than 2: + * { isValidValue: value => { typeof value === 'string' || (typeof value === 'number' && value > 2)} } + * + * A Validator for a number that should be an even number greater than 10 + * { valueType: 'number', validators: [ { isValidValue: v => v > 10 }, { isValidValue: v => v%2 === 0 }] } + * + * @author Sam Reid (PhET Interactive Simulations) + * @author Michael Kauzmann (PhET Interactive Simulations) + */ + +import axon from './axon.js'; +import { ComparableObject } from './TinyProperty.js'; +import { ValueComparisonStrategy } from './Validation.js'; + +export default class ValueComparison { + + public static isEqual( validValue, value, valueComparisonStrategy: ValueComparisonStrategy ): boolean { + + if ( valueComparisonStrategy === 'reference' ) { + return validValue === value; + } + if ( valueComparisonStrategy === 'equalsFunction' ) { + const validComparable = validValue as ComparableObject; + assert && assert( !!validComparable.equals, 'no equals function for 1st arg' ); + assert && assert( !!value.equals, 'no equals function for 2nd arg' ); + assert && assert( validComparable.equals( value ) === value.equals( validComparable ), 'incompatible equality checks' ); + + return validComparable.equals( value ); + } + if ( valueComparisonStrategy === 'lodashDeep' ) { + return _.isEqual( validValue, value ); + } + else { + return valueComparisonStrategy( validValue, value ); + } + } +} +axon.register( 'ValueComparison', ValueComparison ); Index: tandem/js/types/ReferenceArrayIO.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/tandem/js/types/ReferenceArrayIO.ts b/tandem/js/types/ReferenceArrayIO.ts new file mode 100644 --- /dev/null (date 1675372555083) +++ b/tandem/js/types/ReferenceArrayIO.ts (date 1675372555083) @@ -0,0 +1,70 @@ +// Copyright 2018-2022, University of Colorado Boulder + +/** + * IO Type for JS's built-in Array type. + * + * @author Sam Reid (PhET Interactive Simulations) + * @author Andrew Adare (PhET Interactive Simulations) + */ + +import Validation, { ValueComparisonStrategy } from '../../../axon/js/Validation.js'; +import ValueComparison from '../../../axon/js/ValueComparison.js'; +import IntentionalAny from '../../../phet-core/js/types/IntentionalAny.js'; +import tandemNamespace from '../tandemNamespace.js'; +import IOType from './IOType.js'; +import StateSchema from './StateSchema.js'; + +// class Thing{ +// IOType: IOType; +// cache: +// } + +// Cache each parameterized IOType so that it is only created once. +const cache = new Map(); + +/** + * Parametric IO Type constructor. Given an element type, this function returns an appropriate array IO Type. + * This caching implementation should be kept in sync with the other parametric IO Type caching implementations. + */ +const ReferenceDataTypeIO = ( parameterType: IOType, validaValues: ParameterType[], valueComparisonStrategy: ValueComparisonStrategy ): IOType => { + + // const savedValues = new Map(); + + assert && assert( !!parameterType, 'parameterType should be defined' ); + assert && assert( !!validValues, 'include valid validValues please to deserialize back to.' ); + if ( !cache.has( validValues ) ) { + + + cache.set( validValues, new IOType( `ReferenceDataTypeIO<${parameterType.typeName}>`, { + valueType: Array, + isValidValue: array => { + return _.every( array, element => Validation.isValueValid( element, parameterType.validator ) ); + }, + parameterTypes: [ parameterType ], + toStateObject: instance => { + return parameterType.toStateObject( instance ); + }, + fromStateObject: stateObject => { + + const deserialized = parameterType.fromStateObject( stateObject ); + for ( let i = 0; i < validaValues.length; i++ ) { + const possibleValue = validaValues[ i ]; + if ( ValueComparison.isEqual( possibleValue, deserialized, valueComparisonStrategy ) ) { + return possibleValue; + } + } + console.error( 'incorrect deserialization' ); + return validaValues[ 0 ]; + }, + documentation: 'IO Type for the built-in JS array type, with the element type specified.', + stateSchema: StateSchema.asValue( `Array<${parameterType.typeName}>`, { + isValidValue: array => _.every( array, element => parameterType.isStateObjectValid( element ) ) + } ) + } ) ); + } + + return cache.get( validValues )!; +}; + +tandemNamespace.register( 'ReferenceDataTypeIO', ReferenceDataTypeIO ); +export default ReferenceDataTypeIO; \ No newline at end of file Index: calculus-grapher/js/common/model/CalculusGrapherModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/calculus-grapher/js/common/model/CalculusGrapherModel.ts b/calculus-grapher/js/common/model/CalculusGrapherModel.ts --- a/calculus-grapher/js/common/model/CalculusGrapherModel.ts (revision 3b1675cbe3e9d72469e5d2df8f254477eff26f33) +++ b/calculus-grapher/js/common/model/CalculusGrapherModel.ts (date 1675372576696) @@ -30,8 +30,8 @@ import LabeledPoint from './LabeledPoint.js'; import TangentScrubber from './TangentScrubber.js'; import AreaUnderCurveScrubber from './AreaUnderCurveScrubber.js'; -import ArrayIO from '../../../../tandem/js/types/ArrayIO.js'; import EnumerationIO from '../../../../tandem/js/types/EnumerationIO.js'; +import ArrayIO from '../../../../tandem/js/types/ArrayIO.js'; type SelfOptions = { @@ -103,13 +103,12 @@ this.graphSets = options.graphSets; this.graphSetProperty = new Property( options.graphSets[ 0 ], { - validValues: options.graphSets, // Deserializing a value from PhET-iO will result in a difference array instance. We don't care about the actual // array, but instead want to ensure the values of the array is correct. valueComparisonStrategy: 'lodashDeep', tandem: options.tandem.createTandem( 'graphSetProperty' ), - phetioValueType: ArrayIO( EnumerationIO( GraphType ) ) + phetioValueType: ReferenceArrayIO( ArrayIO( EnumerationIO( GraphType ) ), options.graphSets, 'lodashDeep' ) } ); this.curveManipulationProperties = new CurveManipulationProperties( options.curveManipulationModeChoices, { Index: axon/js/Validation.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/axon/js/Validation.ts b/axon/js/Validation.ts --- a/axon/js/Validation.ts (revision 9ba1b065d31b9d8071f458095ea7d54ca0962f79) +++ b/axon/js/Validation.ts (date 1675371630885) @@ -36,7 +36,7 @@ import optionize from '../../phet-core/js/optionize.js'; import IOType from '../../tandem/js/types/IOType.js'; import axon from './axon.js'; -import { ComparableObject } from './TinyProperty.js'; +import ValueComparison from './ValueComparison.js'; const TYPEOF_STRINGS = [ 'string', 'number', 'boolean', 'function' ]; @@ -57,7 +57,7 @@ // allow Function here since it is the appropriate level of abstraction for checking instanceof Function; // eslint-disable-line @typescript-eslint/ban-types -type ValueComparisonStrategy = 'equalsFunction' | 'reference' | 'lodashDeep' | ( ( a: T, b: T ) => boolean ); +export type ValueComparisonStrategy = 'equalsFunction' | 'reference' | 'lodashDeep' | ( ( a: T, b: T ) => boolean ); export type Validator = { @@ -310,32 +310,7 @@ } if ( validator.validValues ) { - - const valueComparisonStrategy: ValueComparisonStrategy = validator.valueComparisonStrategy || 'reference'; - const valueValid = validator.validValues.some( validValue => { - - if ( valueComparisonStrategy === 'reference' ) { - return validValue === value; - } - if ( valueComparisonStrategy === 'equalsFunction' ) { - const validComparable = validValue as ComparableObject; - assert && assert( !!validComparable.equals, 'no equals function for 1st arg' ); - assert && assert( !!value.equals, 'no equals function for 2nd arg' ); - assert && assert( validComparable.equals( value ) === value.equals( validComparable ), 'incompatible equality checks' ); - - return validComparable.equals( value ); - } - if ( valueComparisonStrategy === 'lodashDeep' ) { - return _.isEqual( validValue, value ); - } - else { - return valueComparisonStrategy( validValue, value ); - } - } ); - - if ( !valueValid ) { - return this.combineErrorMessages( `value not in validValues: ${value}`, validator.validationMessage ); - } + ValueComparison.isEqual( validator.validValues, value, validator.valueComparisonStrategy || 'reference' ); } if ( validator.hasOwnProperty( 'isValidValue' ) && !validator.isValidValue!( value ) ) { return this.combineErrorMessages( `value failed isValidValue: ${value}`, validator.validationMessage );
zepumph commented 1 year ago

Something like this for the usage:

phetioValueType: ReferenceDataTypeIO( ArrayIO( EnumerationIO( GraphType ) ), options.graphSets, 'lodashDeep' )

samreid commented 1 year ago

@pixelzoom proposed an alternate solution for Calculus Grapher which seems very appropriate. We may one day have a need for this type, so I'll leave the issue open, but it doesn't block anything at the moment.

zepumph commented 1 year ago

Also over in Greenhouse, we ran into the same thing, but created ReferenceArrayIO specifically. I don't think we need this much generality. Closing