phetsims / wave-on-a-string

"Wave on a String" is an educational simulation in HTML5, by PhET Interactive Simulations.
http://phet.colorado.edu/en/simulation/wave-on-a-string
GNU General Public License v3.0
7 stars 8 forks source link

PhET-iO Instrumentation #138

Open jonathanolson opened 4 years ago

jonathanolson commented 4 years ago

How to Instrument a PhET Simulation for PhET-iO

Before instrumenting

Code Review

A high-quality code review will make instrumentation easier, promote long term maintainability for the simulation, and protect the simulation from a volatile API. If the simulation is already in good shape, the review will not take too long. If the simulation is not in good shape, then it needs your help.

Instrumentation

Now that the simulation is in good shape and the PhET-iO design meeting is complete, we are ready to instrument the simulation. Follow the checklist below, and if you have questions you can review Faraday's Law or Graphing Quadratics and their PhET-iO instrumentations, or reach out to teammates who may have come this way before.

Initial Setup

Visit Objects that Should be Instrumented

Consult the PhET-iO design issue to see what features the sim should support. See PhetioObject.js for the supported PhetioObject options. Not every node in the hierarchy must be instrumented, but every leaf is instrumented. For example the view is rarely instrumented.

Creating and Naming Tandems

Well-designed tandem names are important. Once the PhET-iO simulation is published, the API becomes public and therefore difficult to change. Sometimes PhET-iO design meetings can also help come up tandem names. NOTE: "Tandem" is a PhET internal name, publicly to clients the full strings are known as "phetioIDs" referring to PhET-iO elements.

Feature Support

Create new TypeIOs

If necessary, create new TypeIOs to support desired feature set. Generally we don't want to be locked in to coupling TypeIOs to sim types. Instead, we decided that we want the PhET-iO API to be able to vary independently from the sim implementation instead of leaking sim implementation details (like MultilineText vs Text should both just be TextIO). Still, for a well-designed simulation, TypeIOs will often match closely with the sim types. To ensure good IO type inheritance hierarchies follow these principles:

See https://github.com/phetsims/beers-law-lab/issues/213 for more context on prior problems in this area and discussion about it.

Also note that the since work completed in https://github.com/phetsims/phet-io/issues/1398, PhET-iO interframe communications run on structured cloning (via PostMessage), and not just JSON strings. This means that a failure to implement a proper toStateObject in the TypeIO will result in a hard fail when trying to send instances of that type to the wrapper side. The error will likely look something like this: "Something in message was uncloneable when sent with PostMessage."

The Data Stream

Dynamically created PhET-iO Elements

For simulations that have static content (such as a fixed number of objects and properties), instrumentation is mostly complete and you can skip this section. Not all objects in a phet sim are created on startup. For PhET-iO features and instrumentation, it is much easier to support interoperability for statically created PhET-iO elements, but we also support dynamic elements. For simulations that have a dynamic number of objects, such as Circuit Construction Kit circuits or Molecules and Light photons, the containers and elements must be instrumented. This is currently tricky with PhET-iO. Some sims may wish to avoid this entire hassle by pre-allocating all of the instrumented instances. Consider adding flags to indicate whether the objects are "alive" or "in the pool".

Dynamic elements are now supported through PhetioGroup and PhetioCapsule. These container classes manage validation, api tracking, and dynamic state for their dynamic element(s).

Here is an ordered list of how to approach instrumenting an element that is dynamically created:

  1. Does it even need instrumentation? Often instances don't need to be instrumented, or can perhaps be instrumented as a component of their parent (instead of being instrumented themselves)
  2. Can it be created eagerly? Allocating dynamic elements on startup simplifies the instrumentation process, especially when supporting PhET-iO state and API validation.
  3. Instrument the object dynamically, using PhetioGroup and PhetioCapsule. If you have a use case that is not addressed by one or both of those, then please consult with the PhET-iO subteam, and potentially create a new IO type suitable for your simulation.

Model vs View dynamic elements

When instrumenting dynamic elements, there are two common cases that the PhET-iO has run into in terms of MVC state and serialization support. The first is that the model items need to be instrumented, but the view does not hold any extra data, and so it doesn't need PhET-iO instrumentation. In this case create a PhetioGroup (or PhetioCapsule) for the model, and have the view just listen to the creation and disposal of that element—as should likely already be occurring. A great example of this is john-travoltage, where there are electrons in the model, but the view elements just listen to their respective model element.

If the view for a model element does need to be instrumented, then it is recommended to set up the instrumentation in a specific way.

The PhET-iO team found success with this pattern while working on charges-and-fields. See that sim for example usages of PhetioGroup.

Dispose

Dispose must be implemented properly on all dynamic instances, or else it will result in stale values in the downstream sim. If this is not done correctly, the most common errors will be "phetioID already registered" and "PhetioObject can only be disposed once" errors.

Dispose functions must be added to types that are instrumented. But that's only half of the memory management issue. The other half is revisiting memory management for all instances that don't exist for the lifetime of the sim, and verifying that tandems are properly cleaned up.

PhET-iO State

The state of the simulation has a few uses. Its main purpose is to support saving and loading the simulation. The state of the simulation should be able hold all useful data to get a sim into a useful state. To test the state feature, you can use the "Launch" button in studio, which loads a fresh copy of the sim, and sets the state of the studio customized sim to it. Also in the state wrapper you can play with the upstream sim, and see that state set to the dummy downstream simulation. In both cases, setting the state not only sets the current values, but it also sets the initial values so that resetting the sim/scene/Property will return to the configured initial value rather than the default un-customized simulation state.

PhetioObjects supporting state are serialized via their IO type's toStateObject method. This converts their state to JSON. The fromStateObject deserializes the components of state object (still returning an object literal). An additional call to setValue can be done if deserializing a reference type. See TANDEM/ObjectIO for more documentation.

For PhET-iO state we often refer to the sim instance that is creating the state as the "upstream" sim, and the sim instance that is having state set to it as the "downstream" sim. Note that these could be the same runtime, with a getState call followed by a setState call.

Two types of (de)serialization

Data type serialization For example numbers, strings, Vector2 instances fall into this category. These values are instantiated and returned by fromStateObject.

Reference type serialization For example Nodes and Properties. If a simulation has one heightProperty that exists for the lifetime of the sim, when we save the state of the sim, we save the dynamic characteristics of the heightProperty (rather than trying to serialize the entire list of listeners and phet-io metadata). Then the PhET-iO library calls setValue() to update the dynamic characteristics of the heightProperty without dealing with all of Property's many attributes. The static setValue methods on TypeIOs are automatically called by PhET-iO to restore dynamic characteristics of reference-type serialized instances. In this case, the fromStateObject will not return an instantiated data type, but instead a new object with deserialized components (like a Property's value); this can then be used by setValue.

Search for toStateObject in *IO.js files for examples of both types.

Using Studio to specify metadata

Once the simulation is sufficiently instrumented, tandems are mostly stabilized and appropriate metadata is specified in the code, PhET-iO designers can use studio to specify any remaining metadata.
PhET-iO Studio Instructions describes how to run studio to generate an override file. Some metadata should be provided through the code (like when something really should be phetioReadOnly because it is read only in the code).

Post Instrumentation and Checks

Tips, Tricks, Notes, Misc

Review and Publication

Maintaining

Happy instrumenting!

jonathanolson commented 4 years ago

Preceding dev release: https://phet-dev.colorado.edu/html/wave-on-a-string/1.2.0-dev.1/phet/wave-on-a-string_en_phet.html

jonathanolson commented 4 years ago

Tandems are validating, and I'm at a point where I'll want a phet-io meeting or a review from a phet-io developer before continuing.