TIMESTAMP refrigerator::presync(TIMESTAMP t0, TIMESTAMP t1){ OBJECT *hdr = OBJECTHDR(this); double t = 0.0, dt = 0.0; double nHours = (gl_tohours(t1)- gl_tohours(t0))/TS_SECOND; Tout = *pTout; if(nHours > 0 && t0 > 0){ /* skip this on TS_INIT */ const double COP = COPcoef*((-3.5/45)*(Tout-70)+4.5); /* come from ??? */ if(t1 == next_time){ /* lazy skip-ahead */ load.heatgain = (-((Tair - Tout) * exp(-(UAr+UAf)/Cf) + Tout - Tair) * Cf + Qr * COP) * KWPBTUPH; Tair = Tevent; } else { /* run calculations */ const double C1 = Cf/(UAr+UAf); const double C2 = Tout - Qr/UAr; load.heatgain = (-((Tair - Tout) * exp(-(UAr+UAf)/Cf) + Tout - Tair) * Cf + Qr * COP) * KWPBTUPH;; Tair = (Tair-C2)*exp(-nHours/C1)+C2; } if (Tair < 32 || Tair > 55) throw "refrigerator air temperature out of control"; last_time = t1; } return TS_NEVER; }
TIMESTAMP house::sync(TIMESTAMP t0, TIMESTAMP t1) { OBJECT *obj = OBJECTHDR(this); double nHours = (gl_tohours(t1)- gl_tohours(t0)); //load.energy += load.total * nHours; /* TIMESTAMP sync_time = */ sync_hvac_load(t1, nHours); // sync circuit panel TIMESTAMP panel_time = sync_panel(t0,t1); TIMESTAMP sync_time = sync_thermal(t1, nHours); if (panel_time < sync_time) sync_time = panel_time; /// @todo check panel main breaker (residential, medium priority) (ticket #140) return sync_time; }
TIMESTAMP office::update_plugs(TIMESTAMP t0, TIMESTAMP t1) { //power calculation zone.plugs.enduse.power.SetPowerFactor(zone.plugs.capacity * zone.plugs.fraction, zone.plugs.enduse.power_factor, J); // energy calculation if (t0>0 && t1>t0) zone.plugs.enduse.energy += zone.plugs.enduse.power * gl_tohours(t1-t0); // heatgain calculation zone.plugs.enduse.heatgain = zone.plugs.enduse.power.Mag() * zone.plugs.enduse.heatgain_fraction; return TS_NEVER; }
TIMESTAMP refrigerator::sync(TIMESTAMP t0, TIMESTAMP t1) { double nHours = (gl_tohours(t1)- gl_tohours(t0))/TS_SECOND; double t = 0.0, dt = 0.0; const double COP = COPcoef*((-3.5/45)*(Tout-70)+4.5); // change control mode if appropriate if(motor_state == S_ON){ Qr = rated_capacity; } else if(motor_state == S_OFF){ Qr = 0; } else{ throw "refrigerator motor state is ambiguous"; } // calculate power from motor state load.power = Qr * KWPBTUPH * COP; // compute constants const double C1 = Cf/(UAr+UAf); const double C2 = Tout - Qr/UAr; // compute time to next internal event dt = t = -log((Tevent - C2)/(Tair-C2))*C1; if(t == 0){ GL_THROW("refrigerator control logic error, dt = 0"); } else if(t < 0){ GL_THROW("refrigerator control logic error, dt < 0"); } TIMESTAMP t2 = gl_enduse_sync(&(residential_enduse::load),t1); // if fridge is undersized or time exceeds balance of time or external event pending next_time = (TIMESTAMP)(t1 + (t > 0 ? t : -t) * (3600.0/TS_SECOND) + 1); return next_time > TS_NEVER ? TS_NEVER : -next_time; }
TIMESTAMP house::presync(TIMESTAMP t0, TIMESTAMP t1) { if (t0>0 && t1>t0) load.energy += load.total.Mag() * gl_tohours(t1-t0); // reset accumulators for the next sync /* HVAC accumulators */ load.heatgain = 0; load.total = complex(0,0,J); load.admittance = complex(0,0,J); load.current = complex(0,0,J); /* main panel accumulators */ tload.heatgain = 0; tload.total = complex(0,0,J); tload.admittance = complex(0,0,J); tload.current = complex(0,0,J); return TS_NEVER; }
/** Water heater synchronization determines the time to next synchronization state and the power drawn since last synch **/ TIMESTAMP waterheater::sync(TIMESTAMP t0, TIMESTAMP t1) { double internal_gain = 0.0; double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND; double Tamb = get_Tambient(location); // use re_override to control heat_needed state // runs after thermostat() but before "the usual" calculations if(re_override == OV_ON){ heat_needed = TRUE; } else if(re_override == OV_OFF){ heat_needed = FALSE; } if(Tw > 212.0 - thermostat_deadband){ // if it's trying boil, turn it off! heat_needed = FALSE; is_waterheater_on = 0; } TIMESTAMP t2 = residential_enduse::sync(t0,t1); // Now find our current temperatures and boundary height... // And compute the time to the next transition... //Adjusted because shapers go on sync, not presync set_time_to_transition(); // determine internal gains if (location == INSIDE){ if(this->current_model == ONENODE){ internal_gain = tank_UA * (Tw - get_Tambient(location)); } else if(this->current_model == TWONODE){ internal_gain = tank_UA * (Tw - Tamb) * h / height; internal_gain += tank_UA * (Tlower - Tamb) * (1 - h / height); } } else { internal_gain = 0; } // determine the power used if (heat_needed == TRUE){ /* power_kw */ load.total = (heat_mode == GASHEAT ? gas_fan_power : heating_element_capacity); is_waterheater_on = 1; } else { /* power_kw */ load.total = (heat_mode == GASHEAT ? gas_standby_power : 0.0); is_waterheater_on = 0; } //load.total = load.power = /* power_kw */ load.power; load.power = load.total * load.power_fraction; load.admittance = load.total * load.impedance_fraction; load.current = load.total * load.current_fraction; load.heatgain = internal_gain; waterheater_actual_power = load.power + (load.current + load.admittance * load.voltage_factor )* load.voltage_factor; actual_load = waterheater_actual_power.Re(); if (actual_load != 0.0) { prev_load = actual_load; power_state = PS_ON; } else power_state = PS_OFF; // gl_enduse_sync(&(residential_enduse::load),t1); if(re_override == OV_NORMAL){ if (time_to_transition >= (1.0/3600.0)) // 0.0167 represents one second { TIMESTAMP t_to_trans = (TIMESTAMP)(t1+time_to_transition*3600.0/TS_SECOND); return -(t_to_trans); // negative means soft transition } // less than one second means never else return TS_NEVER; } else { return TS_NEVER; // keep running until the forced state ends } }
/** Water heater plc control code to set the water heater 'heat_needed' state The thermostat set point, deadband, tank state(height of hot water column) and current water temperature are used to determine 'heat_needed' state. **/ TIMESTAMP waterheater::presync(TIMESTAMP t0, TIMESTAMP t1){ /* time has passed ~ calculate internal gains, height change, temperature change */ double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND; OBJECT *my = OBJECTHDR(this); // update temperature and height update_T_and_or_h(nHours); if(Tw > 212.0){ //GL_THROW("the waterheater is boiling!"); gl_warning("waterheater:%i is boiling", my->id); /* TROUBLESHOOT The temperature model for the waterheater has broken, or the environment around the waterheater has burst into flames. Please post this with your model and dump files attached to the bug report. */ } /* determine loadshape effects */ switch(shape.type){ case MT_UNKNOWN: /* normal, undriven behavior. */ break; case MT_ANALOG: if(shape.params.analog.energy == 0.0){ GL_THROW("waterheater does not support fixed energy shaping"); /* TROUBLESHOOT Though it is possible to drive the water demand of a water heater, it is not possible to shape its power or energy draw. Its heater is either on or off, not in between. Change the load shape to not specify the power or energy and try again. */ } else if (shape.params.analog.power == 0){ /* power-driven ~ cheat with W/degF*gpm */ // double heat_per_gallon = RHOWATER * // lb/cf // CFPGAL * // lb/gal // CWATER * // BTU/degF / gal // KWBTUPH / // kW/gal // 1000.0; // W/gal water_demand = gl_get_loadshape_value(&shape) / 2.4449; } else { water_demand = gl_get_loadshape_value(&shape); /* unitless ~ drive gpm */ } break; case MT_PULSED: /* pulsed loadshapes "emit one or more pulses at random times s. t. the total energy is accumulated over the period of the loadshape". * pulsed loadshapes can either user time or kW values per pulse. */ if(shape.params.pulsed.pulsetype == MPT_TIME){ ; /* constant time pulse ~ consumes X gallons to drive heater for Y hours ~ but what's Vdot, what's t? */ } else if(shape.params.pulsed.pulsetype == MPT_POWER){ ; /* constant power pulse ~ draws water to consume X kW, limited by C + Q * h ~ Vdot proportional to power/time */ water_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; case MT_MODULATED: if(shape.params.modulated.pulsetype == MPT_TIME){ GL_THROW("Amplitude modulated water usage is nonsensical for residential water heaters"); /* TROUBLESHOOT Though it is possible to put a constant, low-level water draw on a water heater, it is thoroughly counterintuitive to the normal usage of the waterheater. */ } else if(shape.params.modulated.pulsetype == MPT_POWER){ /* frequency modulated */ /* fixed-amplitude, varying length pulses at regular intervals. */ water_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; case MT_QUEUED: if(shape.params.queued.pulsetype == MPT_TIME){ ; /* constant time pulse ~ consumes X gallons/minute to consume Y thermal energy */ } else if(shape.params.queued.pulsetype == MPT_POWER){ ; /* constant power pulse ~ draws water to consume X kW, limited by C + Q * h */ water_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; default: GL_THROW("waterheater load shape has an unknown state!"); break; } return TS_NEVER; //return residential_enduse::sync(t0,t1); }
TIMESTAMP house::sync_panel(TIMESTAMP t0, TIMESTAMP t1) { TIMESTAMP sync_time = TS_NEVER; OBJECT *obj = OBJECTHDR(this); // clear accumulators for panel currents complex I[3]; I[X12] = I[X23] = I[X13] = complex(0,0); // clear heatgain accumulator double heatgain = 0; // gather load power and compute current for each circuit CIRCUIT *c; for (c=panel.circuits; c!=NULL; c=c->next) { // get circuit type int n = (int)c->type; if (n<0 || n>2) GL_THROW("%s:%d circuit %d has an invalid circuit type (%d)", obj->oclass->name, obj->id, c->id, (int)c->type); /* TROUBLESHOOT Invalid circuit types are an internal error for the house panel. Please report this error. The likely causes include an object that is not a house is being processed by the house model, or the panel was not correctly initialized. */ // if breaker is open and reclose time has arrived if (c->status==BRK_OPEN && t1>=c->reclose) { c->status = BRK_CLOSED; c->reclose = TS_NEVER; sync_time = t1; // must immediately reevaluate devices affected gl_debug("house:%d panel breaker %d closed", obj->id, c->id); } // if breaker is closed if (c->status==BRK_CLOSED) { // compute circuit current if ((c->pV)->Mag() == 0) { gl_debug("house:%d circuit %d (enduse %s) voltage is zero", obj->id, c->id, c->pLoad->name); break; } complex current = ~(c->pLoad->total*1000 / *(c->pV)); // check breaker if (c->max_amps>0 && current.Mag()>c->max_amps) { // probability of breaker failure increases over time if (c->tripsleft>0 && gl_random_bernoulli(RNGSTATE,1/(c->tripsleft--))==0) { // breaker opens c->status = BRK_OPEN; // average five minutes before reclosing, exponentially distributed c->reclose = t1 + (TIMESTAMP)(gl_random_exponential(RNGSTATE,1/300.0)*TS_SECOND); gl_debug("house:%d circuit breaker %d tripped - enduse %s overload at %.0f A", obj->id, c->id, c->pLoad->name, current.Mag()); } // breaker fails from too frequent operation else { c->status = BRK_FAULT; c->reclose = TS_NEVER; gl_debug("house:%d circuit breaker %d failed", obj->id, c->id); } // must immediately reevaluate everything sync_time = t1; } // add to panel current else { tload.power += c->pLoad->power; // reminder: |a| + |b| != |a+b| tload.current += c->pLoad->current; tload.admittance += c->pLoad->admittance; // should this be additive? I don't buy t.a = c->pL->a ... -MH tload.total += c->pLoad->total; tload.heatgain += c->pLoad->heatgain; tload.energy += c->pLoad->power * gl_tohours(t1-t0); I[n] += current; c->reclose = TS_NEVER; } } // sync time if (sync_time > c->reclose) sync_time = c->reclose; } // compute line currents and post to meter if (obj->parent != NULL) LOCK_OBJECT(obj->parent); pLine_I[0] = I[X13]; pLine_I[1] = I[X23]; pLine_I[2] = 0; *pLine12 = I[X12]; if (obj->parent != NULL) UNLOCK_OBJECT(obj->parent); return sync_time; }
/** oven synchronization determines the time to next synchronization state and the power drawn since last synch **/ TIMESTAMP range::sync(TIMESTAMP t0, TIMESTAMP t1) { double internal_gain = 0.0; double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND; double Tamb = get_Tambient(location); double dt = gl_toseconds(t0>0?t1-t0:0); if (oven_check == true || remainon == true) time_oven_operation +=dt; if (remainon == false) time_oven_operation=0; enduse_queue_oven += enduse_demand_oven * dt/3600/24; if (t0>TS_ZERO && t1>t0) { // compute the total energy usage in this interval load.energy += load.total * dt/3600.0; } if(re_override == OV_ON){ heat_needed = TRUE; } else if(re_override == OV_OFF){ heat_needed = FALSE; } if(Tw > 212.0 - thermostat_deadband){ // if it's trying boil, turn it off! heat_needed = FALSE; is_range_on = 0; } // determine the power used if (heat_needed == TRUE){ /* power_kw */ load.total = heating_element_capacity * (heat_mode == GASHEAT ? 0.01 : 1.0); is_range_on = 1; } else { /* power_kw */ load.total = 0.0; is_range_on = 0; } TIMESTAMP t2 = residential_enduse::sync(t0,t1); set_time_to_transition(); if (location == INSIDE){ if(this->current_model == ONENODE){ internal_gain = oven_UA * (Tw - get_Tambient(location)); } } else { internal_gain = 0; } dt = update_state(dt, t1); //load.total = load.power = /* power_kw */ load.power; load.power = load.total * load.power_fraction; load.admittance = load.total * load.impedance_fraction; load.current = load.total * load.current_fraction; load.heatgain = internal_gain; range_actual_power = load.power + (load.current + load.admittance * load.voltage_factor )* load.voltage_factor; actual_load = range_actual_power.Re(); if (heat_needed == true) total_power_oven = actual_load; else total_power_oven =0; if (actual_load != 0.0) { prev_load = actual_load; power_state = PS_ON; } else power_state = PS_OFF; // gl_enduse_sync(&(residential_enduse::load),t1); if(re_override == OV_NORMAL){ if (time_to_transition < dt) { if (time_to_transition >= (1.0/3600.0)) // 0.0167 represents one second { TIMESTAMP t_to_trans = (t1+time_to_transition*3600.0/TS_SECOND); return -(t_to_trans); // negative means soft transition } // less than one second means never else return TS_NEVER; } else return (TIMESTAMP)(t1+dt); } else { return TS_NEVER; // keep running until the forced state ends } }
/** oven plc control code to set the oven 'heat_needed' state The thermostat set point, deadband, oven state and current food temperature are used to determine 'heat_needed' state. **/ TIMESTAMP range::presync(TIMESTAMP t0, TIMESTAMP t1){ /* time has passed ~ calculate internal gains, height change, temperature change */ double nHours = (gl_tohours(t1) - gl_tohours(t0))/TS_SECOND; OBJECT *my = OBJECTHDR(this); // update temperature and height update_T_and_or_h(nHours); if(Tw > 212.0){ //GL_THROW("the range is boiling!"); gl_warning("range:%i is using an experimental model and should not be considered reliable", my->id); /* TROUBLESHOOT The range object has a number of VERY experimental features and development is incomplete. If you are receiving this error message, reccommend no longer using this particular feature without considerable overhaul. */ } /* determine loadshape effects */ switch(shape.type){ case MT_UNKNOWN: /* normal, undriven behavior. */ break; case MT_ANALOG: if(shape.params.analog.energy == 0.0){ GL_THROW("range does not support fixed energy shaping"); /* TROUBLESHOOT Though it is possible to drive the demand of a oven, it is not possible to shape its power or energy draw. Its heater is either on or off, not in between. Change the load shape to not specify the power or energy and try again. */ } else if (shape.params.analog.power == 0){ oven_demand = gl_get_loadshape_value(&shape) / 2.4449; } else { oven_demand = gl_get_loadshape_value(&shape); /* unitless ~ drive gpm */ } break; case MT_PULSED: /* pulsed loadshapes "emit one or more pulses at random times s. t. the total energy is accumulated over the period of the loadshape". * pulsed loadshapes can either user time or kW values per pulse. */ if(shape.params.pulsed.pulsetype == MPT_TIME){ ; /* constant time pulse ~ consumes X gallons to drive heater for Y hours ~ but what's Vdot, what's t? */ } else if(shape.params.pulsed.pulsetype == MPT_POWER){ ; /* constant power pulse ~ oven demand X kW, limited by C + Q * h ~ Vdot proportional to power/time */ oven_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; case MT_MODULATED: if(shape.params.modulated.pulsetype == MPT_TIME){ GL_THROW("Amplitude modulated oven usage is nonsensical for residential ovens"); /* TROUBLESHOOT Though it is possible to put a constant,it is thoroughly counterintuitive to the normal usage of the oven. */ } else if(shape.params.modulated.pulsetype == MPT_POWER){ /* frequency modulated */ /* fixed-amplitude, varying length pulses at regular intervals. */ oven_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; case MT_QUEUED: if(shape.params.queued.pulsetype == MPT_TIME){ ; /* constant time pulse ~ consumes X gallons/minute to consume Y thermal energy */ } else if(shape.params.queued.pulsetype == MPT_POWER){ ; /* constant power pulse ~ range demand X kW, limited by C + Q * h */ oven_demand = gl_get_loadshape_value(&shape) / 2.4449; } break; default: GL_THROW("range load shape has an unknown state!"); break; } return TS_NEVER; //return residential_enduse::sync(t0,t1); }