Skip to content

Builder Pattern API

PyFDS provides a fluent builder pattern API for creating FDS simulations with improved readability and discoverability. Builders offer an alternative to direct namelist construction with chainable methods, smart defaults, and predefined configurations.

Why Use Builders?

Before (Direct Construction)

material = Material(
    id='GYPSUM',
    density=930,
    conductivity=None,
    conductivity_ramp='GYPSUM_K',
    specific_heat=None,
    specific_heat_ramp='GYPSUM_CP',
    emissivity=0.9,
    absorption_coefficient=50000.0,
    n_reactions=2,
    a=[1e10, 5e8],
    e=[80000, 120000],
    heat_of_reaction=[1000, 1500],
    nu_spec=['FUEL_VAPOR', ''],
    nu_matl=['', 'CHAR']
)

After (Builder Pattern)

material = (
    MaterialBuilder('GYPSUM')
    .density(930)
    .thermal_conductivity_ramp('GYPSUM_K')
    .specific_heat_ramp('GYPSUM_CP')
    .add_pyrolysis_reaction(
        a=1e10, e=80000, heat_of_reaction=1000,
        product_species='FUEL_VAPOR'
    )
    .add_pyrolysis_reaction(
        a=5e8, e=120000, heat_of_reaction=1500,
        residue_material='CHAR'
    )
    .build()
)

Key Advantages

  • Readable: Clear parameter names and intent
  • Discoverable: IDE autocomplete guides you through available methods
  • Flexible: Set parameters in any order
  • Validated: Each step can perform validation
  • Smart Defaults: Sensible defaults reduce boilerplate
  • Helpers: Predefined configurations for common scenarios

Available Builders

PyFDS provides six builder classes:

Builder Purpose Usage Pattern
RampBuilder Time-dependent functions Fluent API
MaterialBuilder Thermal and pyrolysis properties Fluent API
ReactionBuilder Combustion reactions Fluent API
ControlBuilder Control logic Fluent API
PropBuilder Device properties Factory methods
VentBuilder Boundary conditions Factory methods

RampBuilder

Create time-dependent functions for fire growth, temperature-dependent properties, and control schedules.

Fire Growth Patterns

from pyfds.builders import RampBuilder

# T-squared fire growth
fire_ramp = (
    RampBuilder('HRR_GROWTH')
    .t_squared('FAST', peak_hrr=2500, t_peak=300)
    .build()
)
sim.add_ramp(fire_ramp)

Available growth rates: 'SLOW', 'MEDIUM', 'FAST', 'ULTRAFAST'

Temperature Tables

For temperature-dependent material properties:

# Temperature-dependent thermal conductivity
k_ramp = (
    RampBuilder('STEEL_K')
    .temperature_table({
        20: 45.8,   # 20°C: 45.8 W/(m·K)
        100: 43.3,
        200: 40.7,
        400: 36.4,
        600: 31.0
    })
    .build()
)
sim.add_ramp(k_ramp)

# Use in material
steel = (
    MaterialBuilder('STEEL')
    .density(7850)
    .thermal_conductivity_ramp('STEEL_K')
    .specific_heat(0.46)
    .build()
)

Other Patterns

# Linear ramp
linear = RampBuilder('LINEAR').linear(t_start=0, t_end=100, f_start=0, f_end=1).build()

# Step function
step = RampBuilder('STEP').step(t_step=60, f_before=0, f_after=1).build()

# Exponential growth
exp = RampBuilder('EXP').exponential(t_start=0, t_end=100, f_start=1, f_end=100, rate=0.05).build()

# Sine wave
sine = RampBuilder('SINE').sine_wave(period=60, amplitude=1.0, offset=0.5, phase=0).build()

# Custom points
custom = RampBuilder('CUSTOM').add_point(0, 0).add_point(10, 1).add_point(20, 0.5).build()

MaterialBuilder

Build materials with thermal properties and pyrolysis reactions.

Simple Materials

from pyfds.builders import MaterialBuilder

# Constant thermal properties
wood = (
    MaterialBuilder('PINE')
    .density(500)
    .thermal_conductivity(0.13)
    .specific_heat(2.5)
    .emissivity(0.9)
    .build()
)
sim.add_material(wood)

Temperature-Dependent Properties

# Using RAMP for temperature-dependent conductivity
steel = (
    MaterialBuilder('STEEL')
    .density(7850)
    .thermal_conductivity_ramp('STEEL_K')  # Reference a RAMP
    .specific_heat(0.46)
    .emissivity(0.7)
    .build()
)

Pyrolysis Materials

# Single-reaction pyrolysis
foam = (
    MaterialBuilder('FOAM')
    .density(40)
    .thermal_conductivity(0.04)
    .specific_heat(1.5)
    .add_pyrolysis_reaction(
        a=1e10,                      # Pre-exponential factor
        e=80000,                     # Activation energy (J/mol)
        heat_of_reaction=1000,       # Heat of reaction (kJ/kg)
        product_species='FUEL_VAPOR' # Product species
    )
    .build()
)

# Multi-reaction pyrolysis
polymer = (
    MaterialBuilder('POLYURETHANE')
    .density(40)
    .thermal_conductivity(0.04)
    .specific_heat(1.5)
    .add_pyrolysis_reaction(
        a=1e10, e=80000, heat_of_reaction=1000,
        product_species='FUEL_VAPOR'
    )
    .add_pyrolysis_reaction(
        a=5e8, e=120000, heat_of_reaction=1500,
        residue_material='CHAR'
    )
    .build()
)

Predefined Materials

# Use predefined common materials
concrete = MaterialBuilder.concrete()
gypsum = MaterialBuilder.gypsum()
steel = MaterialBuilder.steel()
aluminum = MaterialBuilder.aluminum()
brick = MaterialBuilder.brick()
wood = MaterialBuilder.wood()

sim.add_material(concrete)
sim.add_material(gypsum)

Available predefined materials: - concrete() - Concrete (ρ=2400 kg/m³, k=1.6 W/(m·K)) - gypsum() - Gypsum board (ρ=930 kg/m³, k=0.48 W/(m·K)) - steel() - Steel (ρ=7850 kg/m³, k=45.8 W/(m·K)) - aluminum() - Aluminum (ρ=2700 kg/m³, k=237 W/(m·K)) - brick() - Red brick (ρ=1920 kg/m³, k=0.69 W/(m·K)) - wood() - Generic wood (ρ=500 kg/m³, k=0.13 W/(m·K))

ReactionBuilder

Define combustion reactions using predefined fuels or custom compositions.

Using Predefined Fuels

from pyfds.builders import ReactionBuilder

# Use predefined fuel
reac = (
    ReactionBuilder()
    .fuel('PROPANE')
    .yields(soot=0.015, co=0.02)
    .radiative_fraction(0.33)
    .build()
)
sim.add_reaction(reac)

Fuel Database

The builder includes 17 predefined fuels:

Gases: METHANE, ETHANE, PROPANE, BUTANE, HYDROGEN

Liquids: N-HEPTANE, N-HEXANE, GASOLINE, ACETONE, METHANOL, ETHANOL

Solids: POLYURETHANE, WOOD, PMMA, POLYSTYRENE, POLYETHYLENE, POLYPROPYLENE

# List available fuels
fuels = ReactionBuilder.list_fuels()
print(fuels)  # ['ACETONE', 'BUTANE', 'ETHANE', ...]

# Get fuel information
info = ReactionBuilder.get_fuel_info('PROPANE')
print(info)
# {
#   'c': 3, 'h': 8, 'o': 0, 'n': 0,
#   'hoc': 46000,  # kJ/kg
#   'soot_yield': 0.01,
#   'co_yield': 0.004,
#   'description': 'Propane gas'
# }

Custom Fuel Composition

# Define custom fuel
reac = (
    ReactionBuilder()
    .custom_fuel(
        c=7,                        # Carbon atoms
        h=16,                       # Hydrogen atoms
        o=0,                        # Oxygen atoms (optional)
        n=0,                        # Nitrogen atoms (optional)
        heat_of_combustion=44600    # kJ/kg
    )
    .yields(soot=0.01, co=0.02)
    .radiative_fraction(0.30)
    .auto_ignition_temperature(350)
    .build()
)

Configuring Product Yields

reac = (
    ReactionBuilder()
    .fuel('POLYURETHANE')
    .soot_yield(0.10)          # Soot yield (kg soot/kg fuel)
    .co_yield(0.03)            # CO yield (kg CO/kg fuel)
    .radiative_fraction(0.30)  # Radiative fraction (0-1)
    .build()
)

ControlBuilder

Create control logic for device interactions and system automation.

Logic Gates

from pyfds.builders import ControlBuilder

# ANY logic (activates if ANY detector triggers)
alarm = (
    ControlBuilder('SMOKE_ALARM')
    .any(['SMOKE_DET_1', 'SMOKE_DET_2', 'SMOKE_DET_3'])
    .build()
)
sim.add_ctrl(alarm)

# ALL logic (activates if ALL detectors trigger)
multi_zone = (
    ControlBuilder('MULTI_ZONE')
    .all(['ZONE_1_DET', 'ZONE_2_DET'])
    .build()
)

# ONLY logic (passthrough single device)
passthrough = (
    ControlBuilder('PASSTHROUGH')
    .only('HEAT_DET_1')
    .build()
)

Time Delays

# Delayed activation
delayed_sprinkler = (
    ControlBuilder('DELAYED_SPRINKLER')
    .time_delay('HEAT_DETECTOR', delay=10.0)  # 10 second delay
    .build()
)

Modifiers

# Complex control with modifiers
ctrl = (
    ControlBuilder('COMPLEX_CTRL')
    .any(['SD_1', 'SD_2'])
    .with_delay(3.0)              # 3 second delay
    .with_initial_state(False)    # Start deactivated
    .with_latch(True)             # Stay on once activated
    .build()
)

Special Functions

# KILL function (stops simulation)
kill_ctrl = (
    ControlBuilder('KILL')
    .kill(on_t=600)  # Kill at t=600s
    .build()
)

# RESTART function
restart_ctrl = (
    ControlBuilder('RESTART')
    .restart(on_t=300)  # Restart at t=300s
    .build()
)

# CUSTOM function
custom_ctrl = (
    ControlBuilder('CUSTOM')
    .custom(ramp_id='MY_RAMP')
    .build()
)

PropBuilder

Create device properties using factory methods.

Sprinklers

from pyfds.builders import PropBuilder

# Custom sprinkler
sprinkler = PropBuilder.sprinkler(
    id='CUSTOM_SPRINKLER',
    activation_temp=68,     # °C
    rti=50,                 # Response time index (m·s)^0.5
    flow_rate=60,           # L/min
    k_factor=80             # K-factor (L/min/bar^0.5)
)
sim.add_prop(sprinkler)

# Predefined sprinklers
qr = PropBuilder.quick_response_sprinkler(id='QR_SPRINKLER')
sr = PropBuilder.standard_response_sprinkler(id='SR_SPRINKLER')

Smoke Detectors

# Photoelectric smoke detector
smoke_det = PropBuilder.smoke_detector(
    id='PHOTOELECTRIC',
    activation_obscuration=3.28  # %/ft (UL 268 standard)
)

Heat Detectors

# Heat detector
heat_det = PropBuilder.heat_detector(
    id='HEAT_DET',
    activation_temp=74,  # °C
    rti=5.0              # (m·s)^0.5
)

VentBuilder

Create boundary conditions and openings using factory methods.

Openings

from pyfds.builders import VentBuilder

# Generic opening to ambient
opening = VentBuilder.opening(
    xb=Bounds3D.of(5, 5, 2, 4, 0, 2.1),
    id='DOOR'
)
sim.add_vent(opening)

# Convenience methods for common openings
door = VentBuilder.door(
    x=5.0,
    y_min=2.0,
    y_max=3.0,
    z_min=0.0,    # Optional, default 0.0
    z_max=2.1,    # Optional, default 2.1
    id='DOOR_1'
)

window = VentBuilder.window(
    x=0.0,
    y_min=1.0,
    y_max=2.0,
    z_min=1.0,
    z_max=1.5,
    id='WINDOW_1'
)

HVAC

# Supply vent (positive flow)
supply = VentBuilder.hvac_supply(
    xb=Bounds3D.of(5, 6, 5, 6, 3, 3),
    volume_flow=0.5,  # m³/s
    id='SUPPLY_1'
)

# Exhaust vent (negative flow)
exhaust = VentBuilder.hvac_exhaust(
    xb=Bounds3D.of(0, 1, 0, 1, 3, 3),
    volume_flow=0.3,  # m³/s (automatically negated)
    id='EXHAUST_1'
)

Circular Vents

# Circular burner
burner = VentBuilder.circular_burner(
    center=(0, 0, 0),
    radius=0.5,
    surf_id='FIRE',
    id='BURNER'
)

# Annular (ring) burner
ring = VentBuilder.annular_burner(
    center=(0, 0, 0),
    radius=0.5,
    radius_inner=0.3,
    surf_id='FIRE',
    id='RING_BURNER'
)

Mesh Boundaries

# Open mesh boundary
open_boundary = VentBuilder.mesh_boundary(
    mb='XMIN',
    surf_id='OPEN'
)

# Periodic boundary
periodic = VentBuilder.mesh_boundary(
    mb='XMAX',
    surf_id='PERIODIC',
    mesh_id='MESH_1'  # Optional
)

Predefined Libraries

PyFDS includes predefined libraries for common configurations.

Common Materials

from pyfds.builders.libraries import CommonMaterials

# Get predefined materials
concrete = CommonMaterials.concrete()
steel = CommonMaterials.steel()
gypsum = CommonMaterials.gypsum()
aluminum = CommonMaterials.aluminum()
brick = CommonMaterials.brick()
wood = CommonMaterials.wood()
fiberglass = CommonMaterials.fiberglass_insulation()
ceramic = CommonMaterials.ceramic()
glass = CommonMaterials.glass()
copper = CommonMaterials.copper()

# Add to simulation
sim.add_material(concrete)
sim.add_material(steel)

Common Ramps

from pyfds.builders.libraries import CommonRamps

# T-squared fire growth
slow_fire = CommonRamps.t_squared_slow(peak_hrr=1000)
medium_fire = CommonRamps.t_squared_medium(peak_hrr=2000)
fast_fire = CommonRamps.t_squared_fast(peak_hrr=2500)
ultra_fire = CommonRamps.t_squared_ultrafast(peak_hrr=5000)

# Step function
step = CommonRamps.step_at(t=60, value=1.0)

# Linear ramp
linear = CommonRamps.linear_ramp(t_start=0, t_end=100, f_start=0, f_end=1)

# HVAC schedule (24-hour)
hvac = CommonRamps.hvac_schedule_24h(on_time=8, off_time=18)

Complete Example

Here's a complete apartment fire simulation using builders:

from pyfds import Simulation
from pyfds.builders import (
    RampBuilder, MaterialBuilder, ReactionBuilder,
    ControlBuilder, PropBuilder, VentBuilder
)

# Create simulation
sim = Simulation('apartment_fire', title='Apartment Fire Demo')
sim.add(Time(t_end=600))
sim.add(Mesh(ijk=Grid3D.of(100, 100, 50), xb=Bounds3D.of(0, 10, 0, 10, 0, 5)))

# Add combustion reaction
reac = (
    ReactionBuilder()
    .fuel('POLYURETHANE')
    .yields(soot=0.10, co=0.02)
    .radiative_fraction(0.30)
    .build()
)
sim.add_reaction(reac)

# Add fire growth ramp
fire_ramp = (
    RampBuilder('HRR_GROWTH')
    .t_squared('FAST', peak_hrr=2500, t_peak=300)
    .build()
)
sim.add_ramp(fire_ramp)

# Add temperature-dependent steel
k_ramp = (
    RampBuilder('STEEL_K')
    .temperature_table({20: 45.8, 100: 43.3, 400: 36.4})
    .build()
)
sim.add_ramp(k_ramp)

steel = (
    MaterialBuilder('STEEL')
    .density(7850)
    .thermal_conductivity_ramp('STEEL_K')
    .specific_heat(0.46)
    .emissivity(0.7)
    .build()
)
sim.add_material(steel)

# Add vents
door = VentBuilder.door(x=5.0, y_min=2.0, y_max=3.0, id='DOOR')
supply = VentBuilder.hvac_supply(xb=Bounds3D.of(5, 6, 5, 6, 3, 3), volume_flow=0.5, id='SUPPLY')

sim.add_vent(door)
sim.add_vent(supply)

# Add devices
sprinkler = PropBuilder.quick_response_sprinkler(id='SPRINKLER')
smoke_det = PropBuilder.smoke_detector(id='SMOKE_DET')

sim.add_prop(sprinkler)
sim.add_prop(smoke_det)

# Add control
alarm = (
    ControlBuilder('ALARM')
    .any(['SMOKE_DET_1', 'SMOKE_DET_2'])
    .with_latch(True)
    .build()
)
sim.add_ctrl(alarm)

# Add surfaces
sim.add(Surface(id='FIRE', hrrpua=1000.0, color='RED'))
sim.add(Surface(id='WALL', matl_id='STEEL', thickness=0.012))

# Add geometry
sim.add(Obstruction(xb=Bounds3D.of(4.5, 5.5, 4.5, 5.5, 0, 0.1), surf_id='FIRE'))

# Write FDS file
sim.write('apartment_fire.fds')

Best Practices

1. Use Builders for Readability

Builders make code more readable, especially for complex configurations:

# Good: Clear and readable
material = (
    MaterialBuilder('FOAM')
    .density(40)
    .thermal_conductivity(0.04)
    .specific_heat(1.5)
    .add_pyrolysis_reaction(a=1e10, e=80000, product_species='FUEL')
    .build()
)

# Avoid: All on one line
material = MaterialBuilder('FOAM').density(40).thermal_conductivity(0.04).specific_heat(1.5).build()

2. Leverage Predefined Configurations

Use predefined materials and fuels when available:

# Good: Use predefined
concrete = MaterialBuilder.concrete()
reac = ReactionBuilder().fuel('PROPANE').build()

# Avoid: Manually specifying common materials
concrete = MaterialBuilder('CONCRETE').density(2400).thermal_conductivity(1.6)...

3. Validate Early

Build objects as soon as you're done configuring them to catch errors early:

# Good: Build immediately
steel = MaterialBuilder('STEEL').density(7850).thermal_conductivity(45.8).build()
sim.add_material(steel)

# Avoid: Long chains of operations before building
builder = MaterialBuilder('STEEL')
# ... many lines later ...
steel = builder.build()  # Error discovered late

4. Use Type Hints

Enable IDE autocomplete by using type hints:

from pyfds.builders import MaterialBuilder
from pyfds.core.namelists import Material

material: Material = (
    MaterialBuilder('TEST')
    .density(1000)
    .thermal_conductivity(1.0)
    .build()
)

Migration from Direct Construction

If you have existing code using direct namelist construction, you can migrate gradually:

Step 1: Identify Complex Constructions

Look for namelists with many parameters or complex configurations:

# Complex material with pyrolysis
material = Material(
    id='FOAM',
    density=40,
    conductivity=0.04,
    specific_heat=1.5,
    n_reactions=2,
    a=[1e10, 5e8],
    e=[80000, 120000],
    # ... many more parameters
)

Step 2: Convert to Builder

Replace with builder pattern:

material = (
    MaterialBuilder('FOAM')
    .density(40)
    .thermal_conductivity(0.04)
    .specific_heat(1.5)
    .add_pyrolysis_reaction(a=1e10, e=80000, ...)
    .add_pyrolysis_reaction(a=5e8, e=120000, ...)
    .build()
)

Step 3: Use Predefined Where Possible

Replace common materials/fuels with predefined versions:

# Before
concrete = Material(id='CONCRETE', density=2400, conductivity=1.6, specific_heat=0.88)

# After
concrete = MaterialBuilder.concrete()

See Also