Control Logic (CTRL)¶
Implement automatic control systems and device-based automation in FDS simulations.
Overview¶
Controls (CTRL namelist) enable dynamic simulation behavior based on device measurements or time. Common applications:
- Sprinkler activation based on temperature
- HVAC shutdown when smoke detected
- Door opening/closing sequences
- Suppression system activation
- Complex automated responses
from pyfds import Control
# Simple control: Activate when temperature > 68°C
sim.add(Control(
id='SPRINKLER_CTRL',
input_id='TEMP_SPRINKLER',
setpoint=68.0,
initial_state=False
))
Control Basics¶
Device-Based Controls¶
Activate when a device exceeds a threshold:
from pyfds import Device, Control
from pyfds.core.geometry import Point3D
# Temperature device
sim.add(Device(
id='TEMP_CEILING',
quantity='TEMPERATURE',
xyz=Point3D.of(3, 2, 2.4)
))
# Control activates when temp > 74°C
sim.add(Control(
id='SPRINKLER_ACTIVATION',
input_id='TEMP_CEILING',
setpoint=74.0,
initial_state=False
))
Time-Based Controls¶
Activate at specific times:
Control States¶
Controls have boolean states (True/False):
initial_state=False: Starts inactive, activates when condition metinitial_state=True: Starts active, deactivates when condition met
Control Functions¶
Threshold Detection¶
# Activate when device EXCEEDS setpoint
sim.add(Control(
id='TEMP_HIGH',
input_id='TEMP_DEVICE',
setpoint=80.0,
latch=True # Stay activated once triggered
)
# Activate when device BELOW setpoint
sim.add(Control(
id='TEMP_LOW',
input_id='TEMP_DEVICE',
setpoint=20.0,
latch=False # Can deactivate
)
Latching vs Non-Latching¶
# Latching: Once activated, stays active
sim.add(Control(
id='ALARM',
input_id='SMOKE_DET',
setpoint=0.1,
latch=True # Once smoke detected, alarm stays on
)
# Non-latching: Can activate/deactivate
sim.add(Control(
id='FAN',
input_id='TEMP',
setpoint=30.0,
latch=False # Fan turns on/off as temp fluctuates
)
Delays¶
# Activation delay (must exceed setpoint for 60s)
sim.add(Control(
id='DELAYED_ACTIVATION',
input_id='TEMP',
setpoint=70.0,
delay=60.0 # Wait 60s before activating
)
# Response time (realistic device lag)
sim.add(Control(
id='SPRINKLER',
input_id='TEMP_LINK',
setpoint=68.0,
delay=5.0 # 5s thermal lag
)
Logical Operations¶
AND Logic¶
All inputs must be True:
# Activate only if BOTH conditions met
sim.add(Control(
id='AND_CONTROL',
input_id=['SMOKE_HIGH', 'TEMP_HIGH'],
function_type='AND'
)
OR Logic¶
Any input True activates:
# Activate if ANY condition met
sim.add(Control(
id='OR_CONTROL',
input_id=['SMOKE_DET_1', 'SMOKE_DET_2', 'SMOKE_DET_3'],
function_type='OR'
)
NOT Logic¶
Invert control state:
# Active when input is False
sim.add(Control(
id='NOT_CONTROL',
input_id='NORMAL_OPERATION',
function_type='NOT'
)
XOR Logic¶
Exclusive or (exactly one True):
# Activate if exactly one input True
sim.add(Control(
id='XOR_CONTROL',
input_id=['ZONE_1_ALARM', 'ZONE_2_ALARM'],
function_type='XOR'
)
Common Applications¶
Sprinkler Activation¶
from pyfds import Simulation
sim = Simulation(chid='sprinkler_system')
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(60, 50, 30), xb=Bounds3D.of(0, 6, 0, 5, 0, 3)))
# Fire
sim.add(Surface(id='FIRE', hrrpua=1200.0))
sim.add(Obstruction(xb=Bounds3D.of(2.5, 3.5, 2, 3, 0, 0.1), surf_id='FIRE'))
# Sprinkler head temperature (at ceiling)
sim.add(Device(
id='SPRINKLER_LINK',
quantity='TEMPERATURE',
xyz=Point3D.of(3, 2.5, 2.95)
)
# Control: Activate at 68°C (RTI included in device)
sim.add(Control(
id='SPRINKLER_CTRL',
input_id='SPRINKLER_LINK',
setpoint=68.0,
latch=True # Once activated, stays on
)
# Sprinkler spray surface (controlled)
sim.add(Surface(
id='SPRINKLER_SPRAY',
mass_flux=0.05, # Water spray (kg/m²/s)
ctrl_id='SPRINKLER_CTRL'
)
# Sprinkler vent (activates when control True)
sim.add(Vent(
xb=Bounds3D.of(2.8, 3.2, 2.3, 2.7, 2.95, 2.95),
surf_id='SPRINKLER_SPRAY'
)
sim.write('sprinkler_system.fds')
HVAC Smoke Shutdown¶
sim = Simulation(chid='hvac_shutdown')
sim.add(Time(t_end=900.0))
sim.add(Mesh(ijk=Grid3D.of(80, 60, 30), xb=Bounds3D.of(0, 8, 0, 6, 0, 3)))
# Fire
sim.add(Surface(id='FIRE', hrrpua=1000.0))
sim.add(Obstruction(xb=Bounds3D.of(3.5, 4.5, 2.5, 3.5, 0, 0.1), surf_id='FIRE'))
# Smoke detector at ceiling
sim.add(Device(
id='SMOKE_DET',
quantity='OPTICAL DENSITY',
xyz=Point3D.of(4, 3, 2.9)
)
# Control: Shutdown HVAC when smoke detected (OD > 0.05)
sim.add(Control(
id='HVAC_SHUTDOWN',
input_id='SMOKE_DET',
setpoint=0.05,
latch=True,
initial_state=True # HVAC starts ON
)
# HVAC vents controlled by smoke detector
# Control inverted: HVAC ON when control is True (no smoke)
sim.add(Vent(
xb=Bounds3D.of(1, 1.5, 1, 1.5, 3, 3),
surf_id='HVAC',
volume_flow=0.6,
ctrl_id='HVAC_SHUTDOWN'
)
sim.add(Vent(
xb=Bounds3D.of(6.5, 7, 4.5, 5, 3, 3),
surf_id='HVAC',
volume_flow=-0.5,
ctrl_id='HVAC_SHUTDOWN'
)
sim.write('hvac_shutdown.fds')
Fire Suppression System¶
sim = Simulation(chid='suppression_system')
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(100, 80, 40), xb=Bounds3D.of(0, 10, 0, 8, 0, 4)))
# Fire
sim.add(Surface(id='FIRE', hrrpua=1500.0))
sim.add(Obstruction(xb=Bounds3D.of(4.5, 5.5, 3.5, 4.5, 0, 0.1), surf_id='FIRE'))
# Multiple detectors (OR logic)
detectors = []
for i, (x, y) in enumerate([(3, 3), (5, 3), (7, 3), (5, 5)]):
det_id = f'DET_{i+1}'
sim.add(Device(
id=det_id,
quantity='TEMPERATURE',
xyz=Point3D.of(x, y, 3.9)
)
# Individual detector controls
sim.add(Control(
id=f'CTRL_{i+1}',
input_id=det_id,
setpoint=74.0
)
detectors.append(f'CTRL_{i+1}')
# Master control: ANY detector activates system
sim.add(Control(
id='SUPPRESSION_ACTIVATE',
input_id=detectors,
function_type='OR',
latch=True
)
# Suppression nozzles
nozzles = [(2, 4), (5, 2), (5, 6), (8, 4)]
for i, (x, y) in enumerate(nozzles):
sim.add(Surface(
id=f'NOZZLE_{i+1}',
mass_flux=0.08,
ctrl_id='SUPPRESSION_ACTIVATE'
)
sim.add(Vent(
xb=Bounds3D.of(x-0.2, x+0.2, y-0.2, y+0.2, 3.95, 3.95),
surf_id=f'NOZZLE_{i+1}'
)
sim.write('suppression_system.fds')
Door Opening Sequence¶
sim = Simulation(chid='door_sequence')
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(100, 60, 30), xb=Bounds3D.of(0, 10, 0, 6, 0, 3)))
# Fire in room 1
sim.add(Surface(id='FIRE', hrrpua=1000.0))
sim.add(Obstruction(xb=Bounds3D.of(2, 3, 2.5, 3.5, 0, 0.1), surf_id='FIRE'))
# Timer controls for door opening sequence
# Door 1 opens at t=120s
sim.add(Control(id='DOOR1_OPEN', delay=120.0))
# Door 2 opens at t=240s
sim.add(Control(id='DOOR2_OPEN', delay=240.0))
# Door 3 opens at t=360s
sim.add(Control(id='DOOR3_OPEN', delay=360.0))
# Doors as removable obstructions
# Door 1: Between rooms 1 and 2
sim.add(Obstruction(
xb=Bounds3D.of(4.9, 5.1, 2.5, 3.5, 0, 2.1),
surf_id='INERT',
ctrl_id='DOOR1_OPEN',
removable=True # Removed when control activates
)
# Door 2: Between rooms 2 and 3
sim.add(Obstruction(
xb=Bounds3D.of(7.4, 7.6, 2.5, 3.5, 0, 2.1),
surf_id='INERT',
ctrl_id='DOOR2_OPEN',
removable=True
)
# Door 3: Exit to outside
sim.add(Obstruction(
xb=Bounds3D.of(9.9, 10, 2.5, 3.5, 0, 2.1),
surf_id='INERT',
ctrl_id='DOOR3_OPEN',
removable=True
)
sim.write('door_sequence.fds')
Temperature-Controlled Vent¶
sim = Simulation(chid='temp_vent')
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(60, 50, 30), xb=Bounds3D.of(0, 6, 0, 5, 0, 3)))
# Fire
sim.add(Surface(id='FIRE', hrrpua=1200.0))
sim.add(Obstruction(xb=Bounds3D.of(2.5, 3.5, 2, 3, 0, 0.1), surf_id='FIRE'))
# Temperature sensor at ceiling
sim.add(Device(
id='TEMP_CEILING',
quantity='TEMPERATURE',
xyz=Point3D.of(3, 2.5, 2.9)
)
# Control: Open vent when temp > 100°C
sim.add(Control(
id='VENT_OPEN',
input_id='TEMP_CEILING',
setpoint=100.0,
latch=True
)
# Ceiling vent (initially closed obstruction)
sim.add(Obstruction(
xb=Bounds3D.of(2.5, 3.5, 2, 3, 2.95, 3),
surf_id='INERT',
ctrl_id='VENT_OPEN',
removable=True # Opens (removed) when hot
)
sim.write('temp_vent.fds')
Advanced Controls¶
Multi-Stage Response¶
# Stage 1: Alert at 60°C
sim.add(Control(id='ALERT', input_id='TEMP', setpoint=60.0))
# Stage 2: Evacuate at 80°C
sim.add(Control(id='EVACUATE', input_id='TEMP', setpoint=80.0))
# Stage 3: Suppress at 100°C
sim.add(Control(id='SUPPRESS', input_id='TEMP', setpoint=100.0, latch=True))
Zone-Based System¶
# Multiple zones with independent controls
zones = ['ZONE_A', 'ZONE_B', 'ZONE_C']
for zone in zones:
# Detector in each zone
sim.add(Device(id=f'DET_{zone}', quantity='TEMPERATURE', xyz=(...)))
# Control for each zone
sim.add(Control(
id=f'CTRL_{zone}',
input_id=f'DET_{zone}',
setpoint=74.0,
latch=True
)
# Suppression in each zone
sim.add(Vent(
xb=(...),
surf_id='SUPPRESSION',
ctrl_id=f'CTRL_{zone}'
)
Cascading Activation¶
# Detector 1 triggers first stage
sim.add(Control(id='STAGE_1', input_id='DET_1', setpoint=70.0))
# Stage 1 + Detector 2 triggers stage 2
sim.add(Control(
id='STAGE_2',
input_id=['STAGE_1', 'DET_2_CTRL'],
function_type='AND'
)
# Both stages trigger final response
sim.add(Control(
id='FINAL',
input_id=['STAGE_1', 'STAGE_2'],
function_type='AND',
latch=True
)
Control with RAMPs¶
Combine controls with time-varying properties:
# Fire starts at t=0, grows normally
sim.add(Ramp(id='FIRE_GROWTH', t=[0, 180], f=[0, 1]))
sim.add(Surface(id='FIRE', hrrpua=2000.0, ramp_q='FIRE_GROWTH'))
# Control activates suppression at t=200s
sim.add(Control(id='SUPPRESS', delay=200.0))
# Suppression ramp (immediate full flow)
sim.add(Ramp(id='SUPPRESS_RAMP', t=[0, 1], f=[0, 1]))
# Suppression surface with control and ramp
sim.add(Surface(
id='SUPPRESSION',
mass_flux=0.1,
ctrl_id='SUPPRESS',
ramp_mf='SUPPRESS_RAMP'
)
Best Practices¶
1. Clear Control Logic¶
# Good: Descriptive IDs and comments
# Sprinkler activates when ceiling temperature > 68°C
sim.add(Control(
id='SPRINKLER_ACTIVATION',
input_id='LINK_TEMPERATURE',
setpoint=68.0,
latch=True
)
# Poor: Unclear purpose
sim.add(Control(id='C1', input_id='D1', setpoint=68.0))
2. Realistic Response Times¶
# Include thermal lag for fusible links
sim.add(Control(
id='SPRINKLER',
input_id='LINK_TEMP',
setpoint=68.0,
delay=5.0, # RTI effect approximation
latch=True
)
3. Test Control Logic¶
# Add device to monitor control state
sim.add(Device(
id='CTRL_STATE',
quantity='CONTROL VALUE',
ctrl_id='SPRINKLER_ACTIVATION'
)
4. Use Latching Appropriately¶
# Latch for one-time events (alarms, suppression)
sim.add(Control(id='ALARM', input_id='SMOKE', setpoint=0.1, latch=True))
# Don't latch for cyclical controls (thermostats, pressure relief)
sim.add(Control(id='THERMOSTAT', input_id='TEMP', setpoint=25.0, latch=False))
Common Issues¶
Control not activating
Cause: Setpoint never reached or wrong initial state
Solution: Check device values and initial state
Control activates too early/late
Cause: Incorrect setpoint or missing delay
Solution: Adjust setpoint or add delay
Sprinkler doesn't spray
Cause: Control not linked to surface
Solution: Link control to surface via ctrl_id
Control Function Summary¶
| Function | Description | Example Use |
|---|---|---|
| Threshold | Device > setpoint | Temperature detection |
| Timer | Activate at time | Scheduled events |
| AND | All inputs True | Multi-sensor confirmation |
| OR | Any input True | Multiple detector zones |
| NOT | Invert input | Normally-open logic |
| XOR | Exactly one True | Exclusive zones |
| Latch | Stay activated | One-time suppression |
| Delay | Wait before activate | Thermal lag, debounce |
Next Steps¶
- Devices - Creating device inputs for controls
- RAMP - Time-varying properties with controls
- Examples - Advanced control systems
- Fire Sources - Controlling fire behavior