Skip to content

Commit

Permalink
add some configurability to the equilibrium detection, see #378
Browse files Browse the repository at this point in the history
  • Loading branch information
jbphet committed Dec 20, 2023
1 parent 8af1039 commit 2c841c7
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 33 deletions.
82 changes: 72 additions & 10 deletions js/common/model/EnergyAbsorbingEmittingLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ const AT_EQUILIBRIUM_THRESHOLD = 0.004;
// This constant defines the amount of time that the incoming and outgoing energies have to be equal (within a
// threshold) before deciding that the layer is at thermal equilibrium. This is in seconds, and was empirically
// determined, see https://github.com/phetsims/greenhouse-effect/issues/137.
const EQUILIBRATION_TIME = 2.0;
const DEFAULT_EQUILIBRATION_TIME = 2.0;

// This constant defines the amount of time that the incoming and outgoing energies have to be unequal, with the
// difference exceeding a threshold, before the layer will transition from in-equilibrium to out-of-equilibrium.
const DEFAULT_OUT_OF_EQUILIBRIUM_TIME = 0.02;

// The various substances that this layer can model.
class Substance extends EnumerationValue {
Expand Down Expand Up @@ -84,6 +88,16 @@ type SelfOptions = {
// necessary simplification to have the Earth at a stable initial temperature that is reasonable (i.e. not
// absolute zero).
minimumTemperature?: number;

// The following values, which are both in seconds, are used by the algorithm that determines whether the layer is
// currently in energy equilibrium, meaning that the incoming and outgoing energy are roughly equal. These can be
// adjusted to make the algorithm either more stable (higher values) or more responsive (lower values).
atEquilibriumTime?: number;
outOfEquilibriumTime?: number;

// Whether this layer supports displaying its temperature in the view. This is basically here so that we can have
// the "showTemperatureProperty" in the model when needed, but absent in some cases, so that we don't have
// an unnecessary Property appearing in phet-io.
supportsShowTemperature?: boolean;
};
export type EnergyAbsorbingEmittingLayerOptions = SelfOptions & WithRequired<PhetioObjectOptions, 'tandem'>;
Expand All @@ -105,13 +119,26 @@ class EnergyAbsorbingEmittingLayer extends PhetioObject {
// tracks whether the temperature should be shown in the view
public readonly showTemperatureProperty: Property<boolean>;

// The amount of time that equilibrium must be detected before transitioning to at-equilibrium.
private readonly atEquilibriumTimeThreshold: number;

// The amount of time that this layer has been at equilibrium, meaning that the incoming and outgoing energy are
// equal within a threshold. In seconds.
private atEquilibriumTime: number;

// The amount of time that non-equilibrium must be detected before transitioning to out-of-equilibrium.
private readonly outOfEquilibriumTimeThreshold: number;

// The amount of time that this layer has been out of equilibrium, meaning that the difference between the incoming
// and outgoing energy levels has exceeded the equilibrium threshold.
private outOfEquilibriumTime: number;

// Other fields whose meaning should be reasonably obvious.
private readonly substance: Substance;
private readonly mass: number;
private readonly specificHeatCapacity: number;
public readonly minimumTemperature: number;
public readonly atEquilibriumProperty: BooleanProperty;
private atEquilibriumTime: number;

protected constructor( altitude: number, providedOptions?: EnergyAbsorbingEmittingLayerOptions ) {

Expand All @@ -121,10 +148,10 @@ class EnergyAbsorbingEmittingLayer extends PhetioObject {
substance: Substance.GLASS,

initialEnergyAbsorptionProportion: 1,

minimumTemperature: 0,

supportsShowTemperature: false,
atEquilibriumTime: DEFAULT_EQUILIBRATION_TIME,
outOfEquilibriumTime: DEFAULT_OUT_OF_EQUILIBRIUM_TIME,

// phet-io
phetioReadOnly: true,
Expand Down Expand Up @@ -178,6 +205,9 @@ class EnergyAbsorbingEmittingLayer extends PhetioObject {
this.specificHeatCapacity = options.substance.specificHeatCapacity;
this.minimumTemperature = options.minimumTemperature!;
this.atEquilibriumTime = 0;
this.outOfEquilibriumTime = 0;
this.atEquilibriumTimeThreshold = options.atEquilibriumTime;
this.outOfEquilibriumTimeThreshold = options.outOfEquilibriumTime;
}

/**
Expand Down Expand Up @@ -248,18 +278,48 @@ class EnergyAbsorbingEmittingLayer extends PhetioObject {
// emission.
this.temperatureProperty.set( this.temperatureProperty.value + netTemperatureChange );

// Calculate the energy imbalance, which is the difference between the rate of incoming and outgoing energy per unit
// surface area.
const energyImbalance = Math.abs( absorbedEnergy - totalRadiatedEnergyThisStep ) / SURFACE_AREA / dt;

// Update the state of the at-equilibrium indicator. Being at equilibrium in this model requires that the incoming
// and outgoing energy values are equal for a certain amount of time.
if ( Math.abs( absorbedEnergy - totalRadiatedEnergyThisStep ) / SURFACE_AREA / dt < AT_EQUILIBRIUM_THRESHOLD ) {
this.atEquilibriumTime = Math.min( this.atEquilibriumTime + dt, EQUILIBRATION_TIME );
if ( this.atEquilibriumTime >= EQUILIBRATION_TIME && !this.atEquilibriumProperty.value ) {
this.atEquilibriumProperty.set( true );
if ( energyImbalance < AT_EQUILIBRIUM_THRESHOLD ) {

// This layer is currently in energy equilibrium. First, adjust the time accumulators.
this.atEquilibriumTime = Math.min( this.atEquilibriumTime + dt, this.atEquilibriumTimeThreshold );
this.outOfEquilibriumTime = 0;

// See if it's time for a state change.
if ( !this.atEquilibriumProperty.value && this.atEquilibriumTime >= this.atEquilibriumTimeThreshold ) {

// Transition the at-equilibrium state indicator from out-of-equilibrium to in-equilibrium.
this.atEquilibriumProperty.value = true;
if ( this.altitude === 0 ) {
// console.log( 'transitioning to in' );
}
}
}
else {

// The layer is out of equilibrium for this step. Adjust the time accumulators.
// this.outOfEquilibriumTime = Math.min( this.outOfEquilibriumTime + dt, this.outOfEquilibriumTimeThreshold );
this.outOfEquilibriumTime = this.outOfEquilibriumTime + dt;
if ( this.outOfEquilibriumTime > dt ) {
// console.log( `this.outOfEquilibriumTime = ${this.outOfEquilibriumTime}` );
}
this.atEquilibriumTime = 0;
if ( this.atEquilibriumProperty.value ) {
this.atEquilibriumProperty.set( false );

// See if it's time for a state change. There is a bit of an important special case in this 'if' clause: If the
// layer is at its minimum temperature, don't wait for the accumulator, go ahead and make the out-of-
// equilibrium transition right away. This helps make timely updates for the initial transition, but then
// supports a bit of hysteresis thereafter.
if ( this.atEquilibriumProperty.value &&
( this.temperatureProperty.value === this.minimumTemperature ||
this.outOfEquilibriumTime >= this.outOfEquilibriumTimeThreshold ) ) {

// Transition the at-equilibrium state indicator from in-equilibrium to out-of-equilibrium.
this.atEquilibriumProperty.value = false;
}
}

Expand Down Expand Up @@ -291,6 +351,8 @@ class EnergyAbsorbingEmittingLayer extends PhetioObject {
this.temperatureProperty.reset();
this.atEquilibriumProperty.reset();
this.showTemperatureProperty.value = true;
this.atEquilibriumTime = 0;
this.outOfEquilibriumTime = 0;
}

// statics
Expand Down
29 changes: 10 additions & 19 deletions js/common/model/LayersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import IOType from '../../../../tandem/js/types/IOType.js';
import greenhouseEffect from '../../greenhouseEffect.js';
import GreenhouseEffectConstants from '../GreenhouseEffectConstants.js';
import GreenhouseEffectUtils from '../GreenhouseEffectUtils.js';
import AtmosphereLayer from './AtmosphereLayer.js';
import AtmosphereLayer, { AtmosphereLayerOptions } from './AtmosphereLayer.js';
import Cloud from './Cloud.js';
import EMEnergyPacket, { EMEnergyPacketStateObject } from './EMEnergyPacket.js';
import EnergyAbsorbingEmittingLayer from './EnergyAbsorbingEmittingLayer.js';
Expand All @@ -37,6 +37,7 @@ import NumberIO from '../../../../tandem/js/types/NumberIO.js';
import ReferenceArrayIO from '../../../../tandem/js/types/ReferenceArrayIO.js';
import PhetioObject from '../../../../tandem/js/PhetioObject.js';
import { PhetioState } from '../../../../tandem/js/TandemConstants.js';
import StrictOmit from '../../../../phet-core/js/types/StrictOmit.js';

// constants
const HEIGHT_OF_ATMOSPHERE = 50000; // in meters
Expand All @@ -51,21 +52,15 @@ type SelfOptions = {
// the number of energy absorbing and emitting layers in the atmosphere
numberOfAtmosphereLayers?: number;

// indicates whether the layers in the atmosphere are initially active or inactive
atmosphereLayersInitiallyActive?: boolean;

// proportion of energy that crosses an atmosphere layer that is absorbed, 0 for none, 1 for 100%
initialAtmosphereLayerAbsorptionProportion?: number;

// whether to support showing the temperature of the atmosphere layers, only used if these layers are shown in view
supportShowTemperatureInAtmosphereLayers?: boolean;

// whether a flux meter should be present in this model
fluxMeterPresent?: boolean;

// options that are passed through to the ground layer
groundLayerOptions?: GroundLayerOptions;

// options that are passed through to all atmosphere layers
atmosphereLayerOptions?: StrictOmit<AtmosphereLayerOptions, 'tandem'>;

// options that are passed through to the flux meter if present
fluxMeterOptions?: FluxMeterOptions;

Expand Down Expand Up @@ -134,13 +129,11 @@ class LayersModel extends GreenhouseEffectModel {

const options = optionize<LayersModelOptions, SelfOptions, GreenhouseEffectModelOptions>()( {
numberOfAtmosphereLayers: DEFAULT_NUMBER_OF_ATMOSPHERE_LAYERS,
initialAtmosphereLayerAbsorptionProportion: 0,
atmosphereLayersInitiallyActive: true,
supportShowTemperatureInAtmosphereLayers: false,
fluxMeterPresent: false,
groundLayerOptions: {
tandem: providedOptions.tandem.createTandem( 'groundLayer' )
},
atmosphereLayerOptions: {},
fluxMeterOptions: {
tandem: providedOptions.tandem.createTandem( 'fluxMeter' )
},
Expand Down Expand Up @@ -275,12 +268,10 @@ class LayersModel extends GreenhouseEffectModel {
_.times( options.numberOfAtmosphereLayers, index => {
const atmosphereLayer = new AtmosphereLayer(
distanceBetweenAtmosphereLayers * ( index + 1 ),
{
tandem: this.atmosphereLayersTandem.createTandem( `layer${index + 1}` ),
initiallyActive: options.atmosphereLayersInitiallyActive,
initialEnergyAbsorptionProportion: options.initialAtmosphereLayerAbsorptionProportion,
supportsShowTemperature: options.supportShowTemperatureInAtmosphereLayers
}
combineOptions<AtmosphereLayerOptions>(
{ tandem: this.atmosphereLayersTandem.createTandem( `layer${index + 1}` ) },
options.atmosphereLayerOptions
)
);
this.atmosphereLayers.push( atmosphereLayer );
} );
Expand Down
17 changes: 13 additions & 4 deletions js/layer-model/model/LayerModelModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,23 @@ class LayerModelModel extends LayersModel {

const options = optionize<LayerModelModelOptions, SelfOptions, LayersModelOptions>()( {
numberOfAtmosphereLayers: 3,
initialAtmosphereLayerAbsorptionProportion: INITIAL_ABSORPTION_PROPORTION,
atmosphereLayersInitiallyActive: false,
supportShowTemperatureInAtmosphereLayers: true,
groundLayerOptions: {
initialAlbedo: 0.3,
minimumTemperature: MINIMUM_GROUND_TEMPERATURE,
albedoPhetioReadOnly: false,
tandem: providedOptions.tandem.createTandem( 'groundLayer' )
tandem: providedOptions.tandem.createTandem( 'groundLayer' ),

// Configure the equilibrium detection algorithm to be more stable in this model. This is necessary due to the
// more extreme situations that can be set up on this screen. See
// https://github.com/phetsims/greenhouse-effect/issues/378.
atEquilibriumTime: 3,
outOfEquilibriumTime: 1
},
atmosphereLayerOptions: {
initiallyActive: false,
initialEnergyAbsorptionProportion: INITIAL_ABSORPTION_PROPORTION,
supportsShowTemperature: true,
outOfEquilibriumTime: 1
},
fluxMeterPresent: true,
fluxMeterOptions: {
Expand Down

0 comments on commit 2c841c7

Please sign in to comment.