[x] Publish a pre-instrumentation dev release. This can be useful for identifying whether bugs or memory issues
have been introduced during instrumentation, or were pre-existing. This also creates a benchmark to reference against
for memory-leaks, sim size, performance, etc. Document the dev release in the sim's phet-io github issue.
[x] Understand the goal. Read through The PhET-iO Website Dev Guide
including the link to Graphing Quadratics 1.1. Visit all of the linked wrappers and docs. Test each wrapper,
investigate, report bugs, ask questions!
[x] Understand terminology:
A PhetioObject is a class in the tandem repo. All instrumented instances are PhetioObjects.
A "PhET-iO element" is a public facing instrumented PhetioObject that is interoperable across the PhET-iO message
framework. So every PhET-iO element is a PhetioObject in code, but not all PhetioObjects are PhET-iO elements. It
must have a Tandem (and hence a unique string name called a phetioID) and a phetioType dictating how to
serialize the PhetioObject across the frame. In the PhET-iO Studio wrapper, every interoperable object in the tree
is a PhET-iO element.
[x] Understand the complete PhET-iO feature set and api. In general it can be thought of as 4 items:
Customization: Each PhET-iO element corresponds to a PhetioObject instantiated in the sim along with its
associated TypeIO. For example, SCENERY/Node extends PhetioObject and its default TypeIO is NodeIO. Messages
can be sent to the object via its IO type to customize it.
State: The ability to get and set the state of the simulation.
Data Stream: For data analysis or for visual playbacks.
Dynamic communication: In general this won't need to be adjusted or added to while instrumenting a simulation, but
it can be good to familiarize yourself with basic wrapper making code to understand how PhET-iO is meant to be used.
To get started see the template example in the wrapper index.
[ ] Schedule a PhET-iO design meeting for the simulation to identify what should be customizable/interoperable/data stream and
track it in an issue. For example, see how-to-design-phet-io-features-for-a-simulation.md.
Think about how a researcher or 3rd party may wish to configure the simulation or collect data from it, and make sure
that is supported by the instrumentation. For example, some simulations will need custom higher-level events (such as
whether the user created a parallel circuit), for events that are useful, easy to compute in simulation code and
difficult to compute in wrapper code. Or a simulation may need to be configurable in a way that is not already supported
by the instrumentation you have already completed. These features should be determined in the PhET-iO design meeting.
Sometimes it is preferred to have a skeleton, or developer's "best guess" before this meeting so that there is more to
play with in studio. Use your judgement!
[x] It is best for the simulation's responsible developer to perform the PhET-iO instrumentation. They have important
insight into the structure, history, trade-offs and other important details of the simulation implementation
that will facilitate the instrumentation. If the responsible developer is not available for instrumentation, even a
consulting role would be helpful.
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.
[x] Read through the open issues and be aware of any outstanding problems, future work, etc.
[x] If there is a branch with significant effort, consider merging it before instrumentation.
[ ] Complete any planned refactorings.
[x] Address TODOs in the code
[x] Bring the sim up to standards.
[x] If there are sim components that can be exchanged to use newer common code ones, do so. Consulting phet design patterns may be helpful.
[x] You may want to make a dev release after this step depending on how much changed.
[x] Likely you will want to port old sound libraries from vibe to tambo.
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
[x] Add 'phet-io' as a supportedBrand in the sim's package.json. The ~/github/perennial$ grunt generate-data
script on Bayes will automatically add the simulation to the list of phet-io simulations. This will make it possible to
use phetmarks to launch wrappers for testing. This also will add it to continuous fuzz testing. If this process is
offline (if it doesn't automatically update within 10 minutes), you can run the task yourself and reach out to a member
of the PhET-iO subteam to see if the process is failing. More documentation is available in PERENNIAL/generateData.js
[x] Run grunt update in the sim repo. This will generate the needed phet-io api preload files (baseline + overrides)
to run in phet-io brand. Note that while validating the api, these preloads will need to be kept in sync with the current
state of the repo. This may be more trouble than it's worth while iterating. You can use ?phetioValidateAPI=false to
turn off this validation during iteration. However, if you are iterating in studio, you must have an up-to-date baseline
file, since studio leverages it for metadata.
[x] Import Tandem to main.js, see faradays-law-main.js for an example.
[x] Pass tandem instances to each screen using tandem.createTandem(...)
[x] The PhET-iO Studio wrapper will serve as the foundational approach to understand, test, and implement a phet-io
instrumentation. It displays a list of all "PhET-iO" elements and has controls to interoperate with them. Please note
that studio does not demonstrate the entire suite of phet-io features, and thorough testing of all wrapper suite wrappers
is vital to understanding the intricacies of the instrumentation process and goals (see wrapper index for entire list).
You can remove the ?phetioValidateTandems query string to temporarily get past "Tandem was required but not supplied" issues.
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.
[x] Recursively pass tandems and other PhetioObject options into objects that should be instrumented. Do not
instrument objects that are "implementation details" and do not over-instrument. The goal is to design an API that
balances the power of a broad feature set while still being maintainable.
[x] Instrument user interface components such as Checkbox, HSlider, etc.
[x] Instrument model components such as AXON/Property that are critical to the save state or operation of the sim.
This does not necessarily include "implementation details" that should be hidden from the public API; again a design
meeting may be needed here. (Note that some Property sub-classes utilize options specific for use with PhET-IO, units
in NumberProperty for example, and should be passed where appropriate.)
[ ] Instrument all of the features identified in the simulation PhET-iO design issue.
[x] Subclass PhetioObject when you need to add features not already covered by existing types. Be careful not to
shadow pre-existing attributes in PhetioObject such as tandem, isDisposed, andlinkedElements.
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.
[x] Tandems should be named as we wish clients to see them, and for long-term stability. For maintainability, local
vars should be renamed to match tandem names. Tandem names for some PhET-iO Objects such as Properties and Screens
must end with the appropriate suffix, and are checked automatically with assertions.
[x] The screen's model and view should be named model and view.
Use @param tandem for constructors that don't have an options parameter. This typically includes top-level model
and view types that are specific to the sim.
Use tandem in options object: Tandem.REQUIRED for constructors that already have an options parameter. This default
can be helpful for identifying cases where you have neglected to pass a tandem in (because Tandem.REQUIRED will error
loudly if validating tandems). If a tandem is used transitively (by a dependency) but not in a class itself, then
the option can be declared in the dependency and does not need to be repeated in the class itself. Sometimes including
the tandem option can be clearest, even if it is not used in the class (for example, to indicate whether something should be
instrumented).
[x] For arrays, dynamic instances, or otherwise numbered tandems, use PhetioGroup or PhetioCapsule. See note below
on dynamic elements.
[x] Use tandems statically where necessary (outside of constructors). For instance, static objects that are not created from the main sequence.
See BeersLawSolution.js for an example.
[x] All tandems for a sim should exist under a screen, or in the global section, see Tandem.GLOBAL. Tandems
in the general section are the same for all simulations, like activeProperty.
Feature Support
[x] In addition to passing a Tandem instance, each PhetioObject should be provided a TypeIO. The TypeIO for a
PhetioObject indicates the public api for that PhetioObject instance. Most instrumented common code Types already
have a TypeIO provided as a default option for phetioType.
[x] Where appropriate, create or instrument Property instances to make it possible to get/set a value, so value
changes will appear on the data stream and so the item can be stored and restored in save/load. This is preferred to
creating a new TypeIO and implementing get/set within that type.
[x] Each TypeIO has a validator static attribute that can be used to validate the type. When instrumenting a
Property, Emitter, or other type that validates parameters in which that instance provided valueType for validation,
in most cases the TypeIO's validator will be redundant to the valueType field. If this is the case, the valueType
can and should be removed to keep the code simpler and more maintainable.
[x] If necessary, instrument common code components that are not yet instrumented. You can check if something is instrumented
by checking whether it extends PhetioObject and whether it supplies any PhetioObject options. To instrument a new common
code component, you may need to add instrumented Property or Emitter elements by composition, or subclass PhetioObject.
Run phetmarks=>aqua=>Test Sims(Fast Build) with PhET-iO checked. This will help catch any simulations using the component
you just instrumented.
[x] Add tandem: Tandem.REQUIRED or tandem: Tandem.OPTIONAL to the options accordingly. Here are some conventions to
guide this decision:
Most UI components are Tandem.REQUIRED
Even if a common code component that your subtype extends is a certain option, it is safest to set this tandem constraint
at the subtype level in sim specific code too.
The safest convention for required tandems is to add tandem: Tandem.REQUIRED to default options wherever you
intend to pass tandem via options.
[x] Note Node already extends PhetioObject--its PhetioObject options should be provided to the constructor or mutate
but not both.
[x] Use the phetioPrintMissingTandems flag if you want to collect a list of all required, optional, and uninstrumented
common code classes instead of erroring out on the first missing tandem. Each occurrence is numbered to give a better
idea of how many the sim has to do.
[x] Transient state should not be saved. For instance, whether a button is highlighted from mouseover, or whether
the About dialog is showing should not be part of the save state of the simulation. Omit these instances from state with phetioState:false.
[x] Run the simulation with ?phetioValidateTandems to see if you missed anything that should be instrumented.
[x] Add the simulation to perennial/data/testable-phet-io-validated so CT will test with ?phetioValidateTandems
[ ] Use the following conventions regarding phetioDocumentation option:
Documentation strings do not have to be in complete sentences, i.e. "The location of the center of the bar magnet in
view coordinates" is appropriate even though it is just a subject.
Following up on this, the above example does not need (and shouldn't have) a period because it isn't a sentence.
We want phetioDocumentation that is client facing to start with a capital letter.
We allow HTML in our documentation strings to be rendered by the wrapper.
There is no need to restrict any characters, if chars break the HTML, it should be manually verified in Studio before publication.
For now we there isn't validation, only manual inspection as you instrument.
[ ] For UI components, consider whether to link to the underlying Property via addLinkedElement
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:
factor out duplicated code or responsibilities
have the sim developer involved in instrumentation
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
[ ] Create Emitter instances as appropriate to augment the data stream.
[ ] Instrumented Emitters and Property instances naturally emit to a structured data stream and are probably what you need.
If you need something more custom, you can call PhetioObject.phetioStartEvent and PhetioObject.phetioEndEvent directly.
[ ] New code should use Emitter.addListener instead of Events.onStatic
[ ] To suppress an Emitter.emit argument, see the phetioPrivate option in Action.js.
[ ] When instrumenting new types, make sure that events are marked as phetioEventType: 'user' for pointer events, keyboard events and UI events
(like checkbox toggled, button pressed), and phetioEventType: 'model' for model actions/responses. This is easiest to
test in the console: colorized wrapper. Model events will be logged in black, and user events will be logged blue. You can also
go to the data-stream wrapper to see events in JSON form. If your simulation only leverages existing model types (like
Property/Emitter) and UI types (like sun components), then you will not be instrumenting new types.
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:
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)
Can it be created eagerly? Allocating dynamic elements on startup simplifies the instrumentation process, especially
when supporting PhET-iO state and API validation.
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.
Instrument the model types, and support PhET-iO state for them. They should be fully outfitted for save and load.
The view elements will also need PhetioGroup or another container, as they are interoperable dynamic elements.
Instead of being instrumented with a a special IO type though, instrument them as ReferenceIO, such that the state
engine won't be responsible for recreating them when setting the state.
The key to this pattern is that there is already sim code set up for the view to listen to the model when model
elements are created and disposed. So the view should just clear when the model PhetioGroup is cleared (first step of
PhET-iO setting state). Then each new model element dynamically created should trigger a view element to be created.
This is not special PhET-iO code, but rather just the same sim code that MVC normally relies on.
Here is a diagram to help illustrate this pattern:
+
|
upstream | downstream
|
model +---------->
| | |
| | |
| | |
| | |
view v | v
|
+
state
setting
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
[x] Make sure unused PhetioObject instances are disposed, which unregisters the tandem.
[x] Make sure JOIST dt values are used instead of Date.now() or other Date functions. This is necessary for
reproducible playback via input events. Perhaps try phet.joist.elapsedTime.
[x] Are random numbers using phet.joist.random, and all doing so after modules are declared (non-statically)? For
example, the following methods (and perhaps others) should not be used: Math.random, _.shuffle, _.sample, _.random.
[ ] Like JSON, keys for undefined values are omitted in the state--consider this when determining whether
toStateObject should use null or undefined values.
[ ] A nice way to check state is to look at the "changed state" feature in the state wrapper. This displays only the
difference between the state at startup and the current state of the sim. Before continuing make sure that the changed
state makes sense. If it looks like initial values are leaking into the changed state, then it is possible that initialization
has not completed by the time the sim says that construction has ended. In most cases this is a code smell, and could
also be a sneaky bug because we want to make sure that by the time the wrapper gets the onSimInitialized callback, that
the sim has actually been initialized. If there is animating on startup, which causes changed state, that is expected an
alright. See https://github.com/phetsims/phet-io/issues/1555 for more discussion.
[x] Verify that the simulation works in all of the phet-io wrappers. Launch the "index" wrapper at
http://localhost/phet-io-wrappers/index/?sim={{simulation-name}} and test all the links. To further understand what each
wrapper exemplifies, read the description for it in the wrapper index, and launch that wrapper with a sim already
completely PhET-iO instrumented like Faraday's Law.
[ ] Build with grunt --brands=phet-io and test the built version by launching the compiled wrapper index at build/phet-io/, and testing all the links.
[x] Manually look through Studio to make sure that all PhET-iO Elements work as expected and are formatted correctly.
[ ] Perform a full test for memory leaks. The benchmark dev release can be helpful here. This will help catch faulty
tandem disposal. PhET-iO instantiates different objects and wires up listeners that are not present in the PhET-branded
simulation. It needs to be tested separately for memory leaks. Use ?ea&brand=phet-io&phetioStandalone&fuzz to
run with assertions, PhET-iO brand and fuzzing.
[ ] Run phetmarks=>aqua=>Test Sims(Fast Build) with PhET-iO checked. This will help catch any simulations using the
component you just instrumented. Next you will need to pass tandems for those cases.
Tips, Tricks, Notes, Misc
When testing iframes in Chrome, you sometimes must hit refresh twice in order to test your code changes. This is one
reason that testing without iframes, using the `Data: colorized" wrapper is sometimes preferable.
When navigating to wrappers, the easiest way to get to the whole wrapper suite is through the "wrapper index." After
a while of testing it can be annoying to have the extra step: phetmarks --> index --> desired wrapper. Instead you
can use phetmarks to launch any individual wrapper. Note that the wrapper index in the build version is at the top level
of the build dir (build/phet-io/).
Review and Publication
[ ] The PhET-iO instrumentation should be code reviewed, create a new issue.
[ ] Compare performance and memory with pre-instrumentation dev release(s).
[ ] The PhET-iO instrumentation should be QA tested, create a new issue.
[ ] Update these instructions if you find them to be incomplete, inconsistent or incorrect.
How to Instrument a PhET Simulation for PhET-iO
Before instrumenting
/blob/<SHA>/
so that the specific guide you used is preserved. (https://github.com/phetsims/phet-io/blob/afa18dc81d5054235cafe4063b470fc38ac1be52/doc/how-to-instrument-a-phet-simulation-for-phet-io.md)PhetioObject
is a class in thetandem
repo. All instrumented instances arePhetioObject
s.PhetioObject
that is interoperable across the PhET-iO message framework. So every PhET-iO element is aPhetioObject
in code, but not allPhetioObject
s are PhET-iO elements. It must have aTandem
(and hence a unique string name called aphetioID
) and aphetioType
dictating how to serialize thePhetioObject
across the frame. In the PhET-iO Studio wrapper, every interoperable object in the tree is a PhET-iO element.PhetioObject
instantiated in the sim along with its associated TypeIO. For example,SCENERY/Node
extendsPhetioObject
and its default TypeIO isNodeIO
. Messages can be sent to the object via its IO type to customize it.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
package.json
. The~/github/perennial$ grunt generate-data
script on Bayes will automatically add the simulation to the list of phet-io simulations. This will make it possible to use phetmarks to launch wrappers for testing. This also will add it to continuous fuzz testing. If this process is offline (if it doesn't automatically update within 10 minutes), you can run the task yourself and reach out to a member of the PhET-iO subteam to see if the process is failing. More documentation is available in PERENNIAL/generateData.jsgrunt update
in the sim repo. This will generate the needed phet-io api preload files (baseline + overrides) to run in phet-io brand. Note that while validating the api, these preloads will need to be kept in sync with the current state of the repo. This may be more trouble than it's worth while iterating. You can use?phetioValidateAPI=false
to turn off this validation during iteration. However, if you are iterating in studio, you must have an up-to-date baseline file, since studio leverages it for metadata.Tandem
tomain.js
, seefaradays-law-main.js
for an example.tandem
instances to each screen usingtandem.createTandem(...)
You can remove the
?phetioValidateTandems
query string to temporarily get past "Tandem was required but not supplied" issues.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.tandem
s and otherPhetioObject
options into objects that should be instrumented. Do not instrument objects that are "implementation details" and do not over-instrument. The goal is to design an API that balances the power of a broad feature set while still being maintainable.units
inNumberProperty
for example, and should be passed where appropriate.)PhetioObject
when you need to add features not already covered by existing types. Be careful not to shadow pre-existing attributes inPhetioObject
such astandem
,isDisposed
, andlinkedElements
.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.
model
andview
.@param tandem
for constructors that don't have an options parameter. This typically includes top-level model and view types that are specific to the sim.Tandem.REQUIRED
for constructors that already have an options parameter. This default can be helpful for identifying cases where you have neglected to pass a tandem in (becauseTandem.REQUIRED
will error loudly if validating tandems). If a tandem is used transitively (by a dependency) but not in a class itself, then the option can be declared in the dependency and does not need to be repeated in the class itself. Sometimes including the tandem option can be clearest, even if it is not used in the class (for example, to indicate whether something should be instrumented).PhetioGroup
orPhetioCapsule
. See note below on dynamic elements.global
section, seeTandem.GLOBAL
. Tandems in thegeneral
section are the same for all simulations, likeactiveProperty
.Feature Support
Tandem
instance, eachPhetioObject
should be provided a TypeIO. The TypeIO for aPhetioObject
indicates the public api for thatPhetioObject
instance. Most instrumented common code Types already have a TypeIO provided as a default option forphetioType
.Property
instances to make it possible to get/set a value, so value changes will appear on the data stream and so the item can be stored and restored in save/load. This is preferred to creating a new TypeIO and implementing get/set within that type.validator
static attribute that can be used to validate the type. When instrumenting aProperty
,Emitter
, or other type that validates parameters in which that instance providedvalueType
for validation, in most cases the TypeIO'svalidator
will be redundant to thevalueType
field. If this is the case, thevalueType
can and should be removed to keep the code simpler and more maintainable.Property
orEmitter
elements by composition, or subclassPhetioObject
. Run phetmarks=>aqua=>Test Sims(Fast Build) with PhET-iO checked. This will help catch any simulations using the component you just instrumented.tandem: Tandem.REQUIRED
ortandem: Tandem.OPTIONAL
to the options accordingly. Here are some conventions to guide this decision:Tandem.REQUIRED
tandem: Tandem.REQUIRED
to default options wherever you intend to pass tandem via options.Node
already extendsPhetioObject
--itsPhetioObject
options should be provided to the constructor ormutate
but not both.phetioPrintMissingTandems
flag if you want to collect a list of all required, optional, and uninstrumented common code classes instead of erroring out on the first missing tandem. Each occurrence is numbered to give a better idea of how many the sim has to do.phetioState:false
.phetioDocumentation
option:phetioDocumentation
that is client facing to start with a capital letter.DEFAULT_OPTIONS
or at least be very careful about how it is done, see the concerns mentioned in https://github.com/phetsims/phet-io/issues/1179Property
viaaddLinkedElement
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 propertoStateObject
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
Emitter
instances as appropriate to augment the data stream.Emitters
andProperty
instances naturally emit to a structured data stream and are probably what you need. If you need something more custom, you can callPhetioObject.phetioStartEvent
andPhetioObject.phetioEndEvent
directly.Emitter.addListener
instead ofEvents.onStatic
Emitter.emit
argument, see thephetioPrivate
option inAction.js
.phetioEventType: 'user'
for pointer events, keyboard events and UI events (like checkbox toggled, button pressed), andphetioEventType: 'model'
for model actions/responses. This is easiest to test in the console: colorized wrapper. Model events will be logged in black, and user events will be logged blue. You can also go to the data-stream wrapper to see events in JSON form. If your simulation only leverages existing model types (like Property/Emitter) and UI types (like sun components), then you will not be instrumenting new types.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
andPhetioCapsule
. 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:
PhetioGroup
andPhetioCapsule
. 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
(orPhetioCapsule
) 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.
PhetioGroup
or another container, as they are interoperable dynamic elements.Instead of being instrumented with a a special IO type though, instrument them as
ReferenceIO
, such that the state engine won't be responsible for recreating them when setting the state.PhetioGroup
is cleared (first step of PhET-iO setting state). Then each new model element dynamically created should trigger a view element to be created. This is not special PhET-iO code, but rather just the same sim code that MVC normally relies on.Here is a diagram to help illustrate this pattern:
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. ThefromStateObject
deserializes the components of state object (still returning an object literal). An additional call tosetValue
can be done if deserializing a reference type. SeeTANDEM/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 asetState
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 theheightProperty
(rather than trying to serialize the entire list of listeners and phet-io metadata). Then the PhET-iO library callssetValue()
to update the dynamic characteristics of theheightProperty
without dealing with all of Property's many attributes. The staticsetValue
methods on TypeIOs are automatically called by PhET-iO to restore dynamic characteristics of reference-type serialized instances. In this case, thefromStateObject
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 bysetValue
.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
dispose
d, which unregisters the tandem.dt
values are used instead ofDate.now()
or other Date functions. This is necessary for reproducible playback via input events. Perhaps tryphet.joist.elapsedTime
.phet.joist.random
, and all doing so after modules are declared (non-statically)? For example, the following methods (and perhaps others) should not be used:Math.random
,_.shuffle
,_.sample
,_.random
.undefined
values are omitted in the state--consider this when determining whethertoStateObject
should usenull
orundefined
values.http://localhost/phet-io-wrappers/index/?sim={{simulation-name}}
and test all the links. To further understand what each wrapper exemplifies, read the description for it in the wrapper index, and launch that wrapper with a sim already completely PhET-iO instrumented like Faraday's Law.grunt --brands=phet-io
and test the built version by launching the compiled wrapper index atbuild/phet-io/
, and testing all the links.?ea&brand=phet-io&phetioStandalone&fuzz
to run with assertions, PhET-iO brand and fuzzing.Tips, Tricks, Notes, Misc
phetmarks --> index --> desired wrapper
. Instead you can use phetmarks to launch any individual wrapper. Note that the wrapper index in the build version is at the top level of the build dir (build/phet-io/
).Review and Publication
Maintaining
Happy instrumenting!