Repository: silvxlabs/quicfire-tools (language: python)

Issue:
Integrate Pydantic BaseModels into inputs module

14 transition to object based inputs approach

Add QFire_Advanced_User_Inputs.inp to inputs module
- [x] Class object
- [x] Template file
- [x] documentation json
- [x] docstring
- [x] tests


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..b58c978
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,8 @@
+[flake8]
+ignore =
+    # E501: line too long (we rely on black for this)
+    E501,
+    # Whitespace before ':'. flake8 is wrong when inside slice expressions.
+    E203,
+    # Line break before binary operator. flake8 is wrong and goes against PEP8.
+    W503,
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
new file mode 100644
index 0000000..4556b88
--- /dev/null
+++ b/.github/workflows/pre-commit.yaml
@@ -0,0 +1,16 @@
+name: Linting and Formatting
+on:
+  push:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  pre-commit:
+    name: Run pre-commit
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v3
+        with:
+          python-version: "3.10"
+      - uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index da51b6d..3eb9745 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,4 +29,4 @@ jobs:
         TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
       run: |
         python setup.py sdist bdist_wheel
-        twine upload dist/*
\ No newline at end of file
+        twine upload dist/*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..6d5d07b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,17 @@
+exclude: 'tests/data/'
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: check-yaml
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+  - repo: https://github.com/psf/black
+    rev: 23.1.0
+    hooks:
+    - id: black
+    - id: black-jupyter
+  - repo: https://github.com/PyCQA/flake8
+    rev: 6.0.0
+    hooks:
+      - id: flake8
diff --git a/examples/rechunking.ipynb b/examples/rechunking.ipynb
index 74854fa..66bf153 100644
--- a/examples/rechunking.ipynb
+++ b/examples/rechunking.ipynb
@@ -202,7 +202,11 @@
    },
    "outputs": [],
    "source": [
-    "ds_3d = xr.open_zarr(TMP_PATH / \"test.zarr\", drop_variables=[\"groundfuelheight\", \"mburnt_integ\"], consolidated=False)"
+    "ds_3d = xr.open_zarr(\n",
+    "    TMP_PATH / \"test.zarr\",\n",
+    "    drop_variables=[\"groundfuelheight\", \"mburnt_integ\"],\n",
+    "    consolidated=False,\n",
+    ")"
    ]
   },
   {
diff --git a/examples/rioxarray.ipynb b/examples/rioxarray.ipynb
index 585bcbf..849996c 100644
--- a/examples/rioxarray.ipynb
+++ b/examples/rioxarray.ipynb
@@ -106,7 +106,9 @@
    "source": [
     "# Initialize the SimulationOutputs object\n",
     "sim_outputs = outputs.SimulationOutputs(OUTPUT_PATH, SIM_PARAMS)\n",
-    "zarr_simulation_outputs = sim_outputs.to_zarr(TMP_PATH / \"test.zarr\", outputs=\"mburnt_integ\")"
+    "zarr_simulation_outputs = sim_outputs.to_zarr(\n",
+    "    TMP_PATH / \"test.zarr\", outputs=\"mburnt_integ\"\n",
+    ")"
    ]
   },
   {
@@ -2601,7 +2603,14 @@
     "\n",
     "# Plot the DataArray over the shapefilefig\n",
     "fig, ax = plt.subplots(figsize=(10, 10))\n",
-    "raster.plot(ax=ax, cmap=\"hot_r\", cbar_kwargs={'label': 'Vertically Integrated % Mass Burnt', 'orientation': 'vertical'})\n",
+    "raster.plot(\n",
+    "    ax=ax,\n",
+    "    cmap=\"hot_r\",\n",
+    "    cbar_kwargs={\n",
+    "        \"label\": \"Vertically Integrated % Mass Burnt\",\n",
+    "        \"orientation\": \"vertical\",\n",
+    "    },\n",
+    ")\n",
     "roi.plot(ax=ax, facecolor=\"none\", edgecolor=\"green\", linewidth=4)\n",
     "\n",
     "# Set the new bounds with padding\n",
@@ -2609,7 +2618,7 @@
     "ax.set_ylim(y_min - height * padding, y_max + height * padding)\n",
     "\n",
     "# Add the legend\n",
-    "legend_line = mlines.Line2D([], [], color='green', linewidth=4, label='ROI')\n",
+    "legend_line = mlines.Line2D([], [], color=\"green\", linewidth=4, label=\"ROI\")\n",
     "ax.legend(handles=[legend_line])\n",
     "\n",
     "plt.show()"
diff --git a/quicfire_tools/__init__.py b/quicfire_tools/__init__.py
index 4f58b46..027f0cc 100644
--- a/quicfire_tools/__init__.py
+++ b/quicfire_tools/__init__.py
@@ -1,9 +1,4 @@
-from quicfire_tools.inputs import SimulationInputs
-from quicfire_tools.outputs import SimulationOutputs, OutputFile
+from quicfire_tools.outputs import OutputFile, SimulationOutputs
 from quicfire_tools.parameters import SimulationParameters
 
-
-__all__ = ['SimulationInputs',
-           'SimulationParameters',
-           'SimulationOutputs',
-           'OutputFile']
+__all__ = ["SimulationParameters", "SimulationOutputs", "OutputFile"]
diff --git a/quicfire_tools/ignitions.py b/quicfire_tools/ignitions.py
new file mode 100644
index 0000000..b28682c
--- /dev/null
+++ b/quicfire_tools/ignitions.py
@@ -0,0 +1,90 @@
+"""
+QUIC-Fire Tools Ignitions Module
+"""
+from __future__ import annotations
+
+# Core Imports
+from enum import Enum
+
+# External Imports
+from pydantic import BaseModel, SerializeAsAny
+
+
+class IgnitionSources(int, Enum):
+    rectangle = 1
+    square_ring = 2
+    circular_ring = 3
+    ignite_dat_file = 6
+
+
+class IgnitionType(BaseModel):
+    ignition_flag: SerializeAsAny[IgnitionSources]
+
+    def __str__(self):
+        return (
+            f"{self.ignition_flag.value}\t! 1 = rectangle, "
+            f"2 = square ring, 3 = circular ring, "
+            f"4 = file (QF_Ignitions.inp), "
+            f"5 = time-dependent ignitions (QF_IgnitionPattern.inp), "
+            f"6 = ignite.dat (firetech)"
+        )
+
+
+class RectangleIgnition(IgnitionType):
+    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(1)
+    x_min: float
+    y_min: float
+    x_length: float
+    y_length: float
+
+    def __str__(self):
+        flag_line = super().__str__()
+        locations = (
+            f"{self.x_min}\t! South-west corner in the x-direction\n"
+            f"{self.y_min}\t! South-west corner in the y-direction\n"
+            f"{self.x_length}\t! Length in the x-direction\n"
+            f"{self.y_length}\t! Length in the y-direction"
+        )
+        return flag_line + locations
+
+
+class SquareRingIgnition(IgnitionType):
+    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(2)
+    x_min: float
+    y_min: float
+    x_length: float
+    y_length: float
+    x_width: float
+    y_width: float
+
+    def __str__(self):
+        flag_line = super().__str__()
+        locations = (
+            f"{self.x_min}\t! South-west corner in the x-direction\n"
+            f"{self.y_min}\t! South-west corner in the y-direction\n"
+            f"{self.x_length}\t! Length in the x-direction\n"
+            f"{self.y_length}\t! Length in the y-direction\n"
+            f"{self.x_width}\t! Width in the x-direction\n"
+            f"{self.y_width}\t! Width in the y-direction"
+        )
+        return flag_line + locations
+
+
+class CircularRingIgnition(IgnitionType):
+    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(3)
+    x_min: float
+    y_min: float
+    x_length: float
+    y_length: float
+    ring_width: float
+
+    def __str__(self):
+        flag_line = super().__str__()
+        locations = (
+            f"{self.x_min}\t! South-west corner in the x-direction\n"
+            f"{self.y_min}\t! South-west corner in the y-direction\n"
+            f"{self.x_length}\t! Length in the x-direction\n"
+            f"{self.y_length}\t! Length in the y-direction\n"
+            f"{self.ring_width}\t! Width of the ring"
+        )
+        return flag_line + locations
diff --git a/quicfire_tools/input-templates/v5.1.1/QFire_Advanced_User_Inputs.inp b/quicfire_tools/input-templates/v5.1.1/QFire_Advanced_User_Inputs.inp
deleted file mode 100644
index 9485541..0000000
--- a/quicfire_tools/input-templates/v5.1.1/QFire_Advanced_User_Inputs.inp
+++ /dev/null
@@ -1,14 +0,0 @@
-0.05			! N/A, fraction of the cells on fire that will launch a firebrand
-40.			! N/A, scaling factor of the radius represented by the firebrands launched
-1				! s, time step for the firebrands trajectory calculation
-10				! s, how often to launch firebrands
-500			! N/A, number of firebrands distributed over the landing area
-20.			! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
-50.			! N/A, min_b_value_coef
-0.75			! N/A, fb_frac_of_max_size
-180			! s, germination_delay
-5.				! N/A, fraction of the cell on fire (to scale w)
-50				! N/A, minimum number of ignitions via firebrands at a point
-100			! N/A, maximum number of ignitions via firebrands at a point
-0.523598		! rad, min_theta_value (pi/6)
-0.03        ! m, fb thickness
\ No newline at end of file
diff --git a/quicfire_tools/input-templates/v5.1.1/Runtime_Advanced_User_Inputs.inp b/quicfire_tools/input-templates/v5.1.1/Runtime_Advanced_User_Inputs.inp
deleted file mode 100644
index be86501..0000000
--- a/quicfire_tools/input-templates/v5.1.1/Runtime_Advanced_User_Inputs.inp
+++ /dev/null
@@ -1,1 +0,0 @@
-$num_cpus  ! max number of cpu
\ No newline at end of file
diff --git a/quicfire_tools/input-templates/v5.1.1/sensor1.inp b/quicfire_tools/input-templates/v5.1.1/sensor1.inp
deleted file mode 100644
index 3713549..0000000
--- a/quicfire_tools/input-templates/v5.1.1/sensor1.inp
+++ /dev/null
@@ -1,12 +0,0 @@
-sensor1 !Site Name 
-0 !Upper level flag (1 = use this profile for upper level winds)
-50 !Upper level height (meters)
-1 !Site Coordinate Flag (1=QUIC, 2=UTM, 3=Lat/Lon)
-1 !X coordinate (meters)
-1 !Y coordinate (meters)
-$timenow !Begining of time step in Unix Epoch time (integer seconds since 1970/1/1 00:00:00)
-1 !site boundary layer flag (1 = log, 2 = exp, 3 = urban canopy, 4 = discrete data points)
-0.1 !site zo
-0. ! 1/L (default = 0)
-!Height (m),Speed	(m/s), Direction (deg relative to true N)
-2 $wind_speed $wind_direction
diff --git a/quicfire_tools/input-templates/v5.2.3/QFire_Bldg_Advanced_User_Inputs.inp b/quicfire_tools/input-templates/v5.2.3/QFire_Bldg_Advanced_User_Inputs.inp
deleted file mode 100755
index 6f44eeb..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QFire_Bldg_Advanced_User_Inputs.inp
+++ /dev/null
@@ -1,8 +0,0 @@
-1			! N/A, flag to convert QUIC-URB buildings to fuel (0 = no, 1 = yes)
-0.5		! kg/m3, thin fuel density within buildings (if no fuel is specified)
-2.			! N/A, attenuation coefficient within buildings
-0.1	   ! m, surface roughness within buildings
-1			! N/A, flag to convert fuel to canopy for winds (0 = no, 1 = yes)
-1			! N/A, update canopy winds when fuel is consumed (0 = no, 1 = yes)
-1			! N/A, attenuation coefficient within fuel for the Cionco profile (default = 1)
-0.1	   ! m, surface roughness within fuel (default = 0.1 m)
diff --git a/quicfire_tools/input-templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp b/quicfire_tools/input-templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp
deleted file mode 100755
index a1d2b25..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp
+++ /dev/null
@@ -1,16 +0,0 @@
-150000				! N/A, max number of plume at each time step
-0.1				! m/s, minimum vertical velocity of a plume. If wc is below minimum, the plume is eliminated (default 0.1 m/s)
-100				! m/s, maximum vertical velocity of a plume (default 100 m/s)
-0.1				! N/A, minimum speed ratio (plume vertical velocity/wind speed). If below, the plume is eliminated (default 0.1)
-0.					! 1/s2, brunt vaisala frequency squared
-1					! N/A creeping flag: 0 = off, 1 = on
-0					! Flag time step (0 = constant, 1 = adaptive)
-1              ! s, flag = 0: plumes time step plume, flag = 1: minimum plumes time step
-1              ! sor option (0 = reset lambda at each call, 1 = keep)
-10             ! alpha 2 - plume centerline
-2              ! alpha 2 - plume edges
-30.            ! deg, max angle between plumes to merging
-0.7            ! N/A, fraction of overlap of plumes for merging 
-1              ! N/A, which wplume-to-grid scheme: 0 = new, 1 = old
-10             ! N/A, number of points per edge for new scheme (only new scheme)
-1              ! N/A, w computation: 1 = max(w), 0 = sum(w^3)
diff --git a/quicfire_tools/input-templates/v5.2.3/QUIC_fire.inp b/quicfire_tools/input-templates/v5.2.3/QUIC_fire.inp
deleted file mode 100644
index 07bfe9a..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QUIC_fire.inp
+++ /dev/null
@@ -1,43 +0,0 @@
-1					! Fire flag: 1 = run fire; 0 = no fire
--1				    ! Random number generator: -1: use time and date, any other integer > 0 is used as the seed
-! FIRE TIMES
-$timenow		    ! When the fire is ignited in Unix Epoch time (integer seconds since 1970/1/1 00:00:00). Must be greater or equal to the time of the first wind
-$sim_time			! Total simulation time for the fire [s]
-1					! time step for the fire simulation [s]
-1					! Number of fire time steps done before updating the quic wind field (integer, >= 1)
-$output_time		! After how many fire time steps to print out fire-related files (excluding emissions and radiation)
-$output_time		! After how many quic updates to print out wind-related files
-$output_time		! After how many fire time steps to average emissions and radiation
-$output_time		! After how many quic updates to print out averaged wind-related files
-! FIRE GRID
-$nz					! Number of vertical layers of fire grid cells (integer)
-0					! Vertical stretching flag: 0 = uniform dz, 1 = custom
-$dz                 ! m, dz
-! FILE PATH
-""
-1                   ! Fuel types are in separate files
-2                   ! File is stream (1) or headers (2)
-! FUEL
-$fuel_flag		    ! fuel density flag: 1 = uniform; 2 = provided thru QF_FuelDensity.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)    $fuel_density
-$fuel_flag		    ! fuel moisture flag: 1 = uniform; 2 = provided thru QF_FuelMoisture.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)  $fuel_moisture $fuel_height_flag
-! IGNITION LOCATIONS
-$ignition_flag      ! 1 = rectangle, 2 = square ring, 3 = circular ring, 4 = file (QF_Ignitions.inp), 5 = time-dependent ignitions (QF_IgnitionPattern.inp), 6 = ignite.dat (firetech)      $ignition_locations
-2                   ! number of ignitions
-! FIREBRANDS
-0				    ! 0 = off, 1 = on
-! OUTPUT FILES (formats depend on the grid type flag)
-1					! Output gridded energy-to-atmosphere (3D fire grid + extra layers)
-0					! Output compressed array reaction rate (fire grid)
-1					! Output compressed array fuel density (fire grid)
-1					! Output gridded wind (u,v,w,sigma) (3D fire grid)
-1					! Output gridded QU winds with fire effects, instantaneous (QUIC-URB grid)
-0					! Output gridded QU winds with fire effects, averaged (QUIC-URB grid)
-0					! Output plume trajectories (ONLY FOR DEBUG)
-1					! Output compressed array fuel moisture (fire grid)
-1					! Output vertically-integrated % mass burnt (fire grid)
-0					! Output trajectories firebrands
-0					! Output compressed array emissions (fire grid)
-0					! Output gridded thermal radiation (fire grid)
-0                   ! Output surface fire intensity at every fire time step
-! AUTOKILL
-$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
\ No newline at end of file
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_TopoInputs.inp b/quicfire_tools/input-templates/v5.2.3/QU_TopoInputs.inp
deleted file mode 100644
index 87c1cfc..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QU_TopoInputs.inp
+++ /dev/null
@@ -1,11 +0,0 @@
-Specify file name for custom topo (full path)
-"topo.dat"
-$topo_flag     ! N/A, topo flag: 0 = flat, 1 = Gaussian hill, 2 = hill pass, 3 = slope mesa, 4 = canyon, 5 = custom, 6 = half circle, 7 = sinusoid, 8 = cos hill, 9 = QP_elevation.inp, 10 = terrainOutput.txt (ARA), 11 = terrain.dat (firetec)
-1     ! N/A, smoothing method (0 = none (default for idealized terrain), 1 = blur, 2 = David's)
-200     ! N/A, number of smoothing passes
-200   ! N/A, number of initial SOR iterations (only if fire is run)
-1     ! N/A, sor cycles
-0.7   ! N/A, sor relaxation parameter (default for flat is 1.78, ignored if there is no terrain)
-0     ! N/A, add slope flow correction (0 = no, 1 = yes)
-Command to run slope flow code (system specific)
-""
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_buildings.inp b/quicfire_tools/input-templates/v5.2.3/QU_buildings.inp
deleted file mode 100755
index 6dd3867..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QU_buildings.inp
+++ /dev/null
@@ -1,4 +0,0 @@
-!QUIC 6.26
-0.1			!Wall roughness length (m)
-0			!Number of Buildings
-0			!Number of Polygon Building Nodes
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_simparams.inp b/quicfire_tools/input-templates/v5.2.3/QU_simparams.inp
deleted file mode 100755
index 400ed26..0000000
--- a/quicfire_tools/input-templates/v5.2.3/QU_simparams.inp
+++ /dev/null
@@ -1,67 +0,0 @@
-!QUIC 6.26
-$nx !nx - Domain Length(X) Grid Cells
-$ny !ny - Domain Width(Y) Grid Cells
-32 ! NZ - domain height(Z) grid cells
-$dx ! DX [m]
-$dy ! DY [m]
-3 !Vertical stretching flag (0 = uniform, 1 = custom, 2 = parabolic Z, 3 = parabolic DZ, 4 = exponential)
-1.000000 ! Surface DZ [m]
-5 ! Number of uniform surface cells
-! DZ array [m]
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.078399
-1.313596
-1.705591
-2.254385
-2.959976
-3.822366
-4.841553
-6.017539
-7.350323
-8.839905
-10.486285
-12.289463
-14.249439
-16.366213
-18.639786
-21.070156
-23.657325
-26.401292
-29.302056
-32.354352
-35.354352
-38.354352
-41.354352
-44.354352
-47.354352
-50.354352
-53.354352
-1 ! Number of time increments
-0 ! UTC conversion [hours]
-! Begining of time step in Unix epoch time [integer seconds since 1970/1/1 00:00:00]
-$timenow
-2 ! Rooftop flag (0 = none, 1 = log profile, 2 = vortex)
-3 ! Upwind cavity flag (0 = none, 1 = Rockle, 2 = MVP, 3 = HMVP)
-4 ! Street canyon flag (0 = none, 1 = Roeckle, 2 = CPB, 3 = exp. param. PKK, 4 = Roeckle w/ Fackrel)
-1 ! Street intersection flag (0 = off, 1 = on)
-4 ! Wake flag (0 = none, 1 = Rockle, 2 = modified Rockle, 3 = area scaled)
-1 ! Sidewall flag (0 = off, 1 = on)
-1 ! Individual tree wake flag
-2 ! Canopy flag (0 = off, 1 = Cionco w/o wakes, 2 = Cionco w/ wakes)
-1 ! Season flag (1 = Summer, 2 = Winter, 3 = Transition)
-10 ! Maximum number of iterations
-3 ! Residual reduction (Orders of Magnitude)
-0 ! Use diffusion algorithm (0 = off, 1 = on)
-20 ! Number of diffusion iterations
-0 ! Domain rotation relative to true north [degrees] (cw = +)
-0.0 ! UTMX of domain origin [m]
-0.0 ! UTMY of domain origin [m]
-1 ! UTM zone
-17 ! UTM zone leter (1 = A, 2 = B, etc.)
-0 ! QUIC-CFD Flag
-0 ! Explosive building damage flag (1 = on)
-0 ! Building Array Flag (1 = on)
diff --git a/quicfire_tools/input-templates/v5.2.3/Runtime_Advanced_User_Inputs.inp b/quicfire_tools/input-templates/v5.2.3/Runtime_Advanced_User_Inputs.inp
deleted file mode 100755
index 58f4b83..0000000
--- a/quicfire_tools/input-templates/v5.2.3/Runtime_Advanced_User_Inputs.inp
+++ /dev/null
@@ -1,2 +0,0 @@
-$num_cpus  ! max number of cpu threads used
-0  ! Use Adaptive Computation Window (0=Disabled 1=Enabled)
diff --git a/quicfire_tools/input-templates/v5.2.3/gridlist b/quicfire_tools/input-templates/v5.2.3/gridlist
deleted file mode 100644
index 4facaff..0000000
--- a/quicfire_tools/input-templates/v5.2.3/gridlist
+++ /dev/null
@@ -1,8 +0,0 @@
-n=$nx
-m=$ny 
-l=$nz
-aa1=$dz
-nv=8
-dx=$dx
-dy=$dy
-dz=$dz
diff --git a/quicfire_tools/input-templates/v5.2.3/sensor1.inp b/quicfire_tools/input-templates/v5.2.3/sensor1.inp
deleted file mode 100644
index 3713549..0000000
--- a/quicfire_tools/input-templates/v5.2.3/sensor1.inp
+++ /dev/null
@@ -1,12 +0,0 @@
-sensor1 !Site Name 
-0 !Upper level flag (1 = use this profile for upper level winds)
-50 !Upper level height (meters)
-1 !Site Coordinate Flag (1=QUIC, 2=UTM, 3=Lat/Lon)
-1 !X coordinate (meters)
-1 !Y coordinate (meters)
-$timenow !Begining of time step in Unix Epoch time (integer seconds since 1970/1/1 00:00:00)
-1 !site boundary layer flag (1 = log, 2 = exp, 3 = urban canopy, 4 = discrete data points)
-0.1 !site zo
-0. ! 1/L (default = 0)
-!Height (m),Speed	(m/s), Direction (deg relative to true N)
-2 $wind_speed $wind_direction
diff --git a/quicfire_tools/inputs.py b/quicfire_tools/inputs.py
index 057a0ab..bc566ad 100644
--- a/quicfire_tools/inputs.py
+++ b/quicfire_tools/inputs.py
@@ -3,234 +3,1859 @@
 """
 from __future__ import annotations
 
+
 # Core Imports
+import json
 import time
+import importlib.resources
 from pathlib import Path
 from string import Template
+from typing import Literal
+
+# External Imports
+import numpy as np
+from pydantic import (
+    BaseModel,
+    Field,
+    NonNegativeFloat,
+    NonNegativeInt,
+    PositiveFloat,
+    PositiveInt,
+    computed_field,
+    field_validator,
+    SerializeAsAny,
+)
+
+# Internal imports
+from quicfire_tools.ignitions import (
+    CircularRingIgnition,
+    IgnitionType,
+    RectangleIgnition,
+    SquareRingIgnition,
+)
+from quicfire_tools.topography import (
+    CanyonTopo,
+    CosHillTopo,
+    GaussianHillTopo,
+    HalfCircleTopo,
+    HillPassTopo,
+    SinusoidTopo,
+    SlopeMesaTopo,
+    TopoType,
+)
+from quicfire_tools.utils import compute_parabolic_stretched_grid
 
-# Internal Imports
-from quicfire_tools.parameters import SimulationParameters
+
+DOCS_PATH = (
+    importlib.resources.files("quicfire_tools")
+    .joinpath("inputs")
+    .joinpath("documentation")
+)
+TEMPLATES_PATH = (
+    importlib.resources.files("quicfire_tools").joinpath("inputs").joinpath("templates")
+)
 
 
 class SimulationInputs:
     """
-    Input Module
+    Class representing a QUIC-Fire input file deck.
+
+    This is the fundamental class in the quicfire_tools.inputs module. It is
+    used to create, modify, and write QUIC-Fire input file decks. It is also
+    used to read in existing QUIC-Fire input file decks.
     """
 
-    def __init__(self, directory: Path | str):
-        if isinstance(directory, str):
-            path = Path(directory)
-            directory = path.resolve()
+    _required_inputs: list[str] = ["QUIC_fire", "QU_simparams", "gridlist"]
+
+    def __init__(self, input_files: list[InputFile]):
+        # Validate that all required input files are present
+        input_file_names = [input_file.name for input_file in input_files]
+        for required_input in self._required_inputs:
+            if required_input not in input_file_names:
+                raise ValueError(f"Missing required input file: {required_input}")
 
-        directory.mkdir(parents=True, exist_ok=True)
-        self.directory = directory
+        # Initialize a dictionary of input files
+        self._inputs = {i.name: i for i in input_files}
 
-    def setup_input_files(self, params):
+    def list_inputs(self) -> list[str]:
         """
-        Populates input file templates with user defined parameters
+        List all input files by name in the SimulationInputs object.
+
+        Returns
+        -------
+        list[str]
+            List of input file names.
+        """
+        return list(self._inputs.keys())
+
+    def get_input(self, input_name: str) -> InputFile:
+        """
+        Retrieve an input file object by name from the SimulationInputs object.
 
         Parameters
         ----------
-        params: SimulationParameters
-            Dictionary of user defined parameters.
+        input_name: str
+            Name of the input file to retrieve.
 
         Returns
         -------
-        None:
-            Sets up simulation files in the simulation directory.
+        InputFile
+            Input file object.
+        """
+        return self._inputs[input_name]
 
+    def write_inputs(self, directory: str | Path, version: str = "latest") -> None:
         """
-        # Convert params to dict
-        params = params.to_dict()
+        Write all input files in the SimulationInputs object to a specified
+        directory.
 
-        # Get current unix time
-        params["timenow"] = int(time.time())
+        This method is the core method of the SimulationInputs class. It
+        is the principle way to translate a SimulationInputs object into a
+        QUIC-Fire input file deck.
 
-        # Write fuels data
-        (params["fuel_density"], params["fuel_moisture"],
-         params["fuel_height_flag"]) = self._write_fuel(params)
+        Parameters
+        ----------
+        directory: str | Path
+            Directory to write the input files to.
+        version: str
+            Version of the input files to write. Default is "latest".
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        if not directory.exists():
+            raise NotADirectoryError(f"{directory} does not exist")
 
-        # Write ignition data
-        params["ignition_locations"] = self._write_ignition_locations(params)
+        for input_name in self.list_inputs():
+            input_file = self.get_input(input_name)
+            input_file.to_file(directory, version=version)
 
-        # Write input template files
-        try:
-            version = params["version"]
-        except KeyError:
-            version = "latest"
-        template_files_path = Path(
-            __file__).parent / "input-templates" / version
-        template_files_list = template_files_path.glob("*")
-        for fname in template_files_list:
-            self._fill_form_with_dict(fname, params)
+        # Copy QU_landuse from the template directory to the output directory
+        template_file_path = TEMPLATES_PATH / version / "QU_landuse.inp"
+        output_file_path = directory / "QU_landuse.inp"
+        with open(template_file_path, "rb") as ftemp:
+            with open(output_file_path, "wb") as fout:
+                fout.write(ftemp.read())
 
-    @staticmethod
-    def _write_fuel(params: dict) -> tuple[str, str, str]:
+    @classmethod
+    def setup_simulation(
+        cls,
+        nx: int,
+        ny: int,
+        fire_nz: int,
+        quic_nz: int,
+        quic_height: float,
+        dx: float,
+        dy: float,
+        fire_dz: float,
+        wind_speed: float,
+        wind_direction: float,
+        simulation_time: int,
+        output_time: int,
+    ):
         """
-        Writes fuel data to the QUIC_fire.inp input file. This function
+        Creates a SimulationInputs object with the minimum required inputs to
+        build a QUIC-Fire input file deck and run a simulation.
 
         Parameters
         ----------
-        params: dict
-            Dictionary of user defined parameters.
+        nx: int
+            Number of cells in the x-direction [-]
+        ny: int
+            Number of cells in the y-direction [-]
+        fire_nz: int
+            Number of cells in the z-direction for the fire grid [-]
+        quic_nz: int
+            Number of cells in the z-direction for the QUIC grid [-]
+        dx: float
+            Cell size in the x-direction [m]
+        dy: float
+            Cell size in the y-direction [m]
+        fire_dz: float
+            Cell size in the z-direction for the fire grid [m]
+        quic_height: float
+            QUIC domain height [m]. Determines the cell size in the z-direction
+            for the QUIC grid.
+        wind_speed: float
+            Wind speed [m/s]
+        wind_direction: float
+            Wind direction [deg]. 0 deg is north, 90 deg is east, etc. Must
+            be in range [0, 360).
+        simulation_time: int
+            Number of seconds to run the simulation for [s]
+        output_time: int
+            Number of seconds between output files [s]
 
         Returns
         -------
-        None:
-            Writes fuel data to the fuel_data.inp file.
-
+        SimulationInputs
+            Class containing the minimum required inputs to build a QUIC-Fire
+            input file deck and run a simulation.
         """
-        # Get data from params
-        fuel_flag = params["fuel_flag"]
+        # Initialize default input files
+        raster_origin = RasterOrigin()
+        qu_bldgs = QU_Buildings()
+        qu_fileoptions = QU_Fileoptions()
+        qfire_adv_user_input = QFire_Advanced_User_Inputs()
+        qfire_bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        qfire_plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        qu_topo = QU_TopoInputs()
+        runtime_inputs = RuntimeAdvancedUserInputs()
+        qu_movingcoords = QU_movingcoords()
+        qp_buildout = QP_buildout()
+        qu_metparams = QU_metparams()
 
-        # Uniform fuel properties
-        if fuel_flag == 1:
-            fuel_density = "\n" + str(params["fuel_density"])
-            fuel_moisture = "\n" + str(params["fuel_moisture"])
-            fuel_height = (f"\n{fuel_flag}    ! fuel height flag: 1 = uniform; "
-                           f"2 = provided thru QF_FuelMoisture.inp, 3 = Firetech"
-                           f" files for quic grid, 4 = Firetech files for "
-                           f"different grid (need interpolation)"
-                           f"\n{str(params['fuel_height'])}")
-
-        # Custom fuel .dat files (fuel flags 3 or 4)
-        else:
-            fuel_density, fuel_moisture, fuel_height = "", "", ""
+        # Initialize input files with required parameters
+        start_time = int(time.time())
+        qu_simparams = QU_Simparams(
+            nx=nx, ny=ny, nz=quic_nz, dx=dx, dy=dy, quic_domain_height=quic_height
+        )
+        quic_fire = QUIC_fire(
+            nz=fire_nz,
+            time_now=start_time,
+            sim_time=simulation_time,
+            out_time_fire=output_time,
+            out_time_wind=output_time,
+            out_time_emis_rad=output_time,
+            out_time_wind_avg=output_time,
+        )
+        gridlist = Gridlist(n=nx, m=ny, l=fire_nz, dx=dx, dy=dy, dz=fire_dz, aa1=1.0)
+        wind_sensor = Sensor1(
+            time_now=start_time, wind_speed=wind_speed, wind_direction=wind_direction
+        )
+
+        input_files = [
+            raster_origin,
+            qu_bldgs,
+            qu_fileoptions,
+            qfire_adv_user_input,
+            qfire_bldg_inputs,
+            qfire_plume_inputs,
+            qu_topo,
+            runtime_inputs,
+            qu_movingcoords,
+            qp_buildout,
+            qu_metparams,
+            qu_simparams,
+            quic_fire,
+            gridlist,
+            wind_sensor,
+        ]
 
-        return fuel_density, fuel_moisture, fuel_height
+        return cls(input_files)
 
-    def _write_ignition_locations(self, params: dict) -> str:
+    @classmethod
+    def from_directory(cls, directory: str | Path) -> SimulationInputs:
         """
-        Writes ignition locations file to simulation directory if ignition flag
-        is 6. Otherwise, it writes the ignition locations to the QUIC_fire.inp
-        input file.
+        Initializes a SimulationInputs object from a directory containing a
+        QUIC-Fire input file deck.
 
         Parameters
         ----------
-        params: dict
-            Dictionary of user defined parameters.
+        directory: str | Path
+            Directory containing a QUIC-Fire input file deck.
 
         Returns
         -------
-        ignition_locations: str
-            Data string to write to the QUIC_fire.inp input file.
+        SimulationInputs
+            Class containing the input files in the QUIC-Fire input file deck.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        input_files = []
+        for input_file_name in cls._required_inputs:
+            input_obj = globals()[input_file_name]
+            input_file = input_obj.from_file(directory)
+            input_files.append(input_file)
+        return cls(input_files)
 
+
+class InputFile(BaseModel, validate_assignment=True):
+    """
+    Base class representing an input file.
+
+    This base class provides a common interface for all input files in order to
+    accomplish two main goals:
+    1) Return documentation for each parameter in the input file.
+    2) Provide a method to write the input file to a specified directory.
+    """
+
+    name: str
+    _extension: str
+    _param_info: dict = None
+
+    @property
+    def _filename(self):
+        return f"{self.name}{self._extension}"
+
+    @property
+    def param_info(self):
+        """
+        Return a dictionary of parameter information for the input file.
         """
-        if params["ignition_flag"] == 6 or params["ignition_flag"] == 7:
-            return ""
+        if self._param_info is None:  # open the file if it hasn't been read in
+            with open(DOCS_PATH / f"{self._filename}.json", "r") as f:
+                self._param_info = json.load(f)
+        return self._param_info
 
-        else:  # Ignition flag 1
-            return self._write_line_fire_ignition(params)
+    def list_parameters(self):
+        """List all parameters in the input file."""
+        return list(self.param_info.keys())
 
-    @staticmethod
-    def _write_line_fire_ignition(params: dict) -> str:
+    def get_documentation(self, parameter: str = None):
         """
-        Writes line fire ignition locations to the QUIC_fire.inp input file.
+        Retrieve documentation for a parameter. If no parameter is specified,
+        return documentation for all parameters.
+        """
+        if parameter:
+            return self.param_info.get(parameter, {})
+        else:
+            return self.param_info
 
-        Line fire ignition takes the following parameters:
-        fire_source_x0: south-west corner in the x-direction
-        fire_source_y0: south-west corner in the y-direction
-        fire_source_xlen: length in the x-direction
-        fire_source_ylen: length in the y-direction
-        num_ignition_cells: number of ignition cells constant at 100
+    def print_documentation(self, parameter: str = None):
+        """
+        Print documentation for a parameter. If no parameter is specified,
+        print documentation for all parameters.
+        """
+        if parameter:
+            info = self.get_documentation(parameter)
+        else:
+            info = self.get_documentation()
+        for key, value in info.items():
+            key = key.replace("_", " ").capitalize()
+            print(f"- {key}: {value}")
 
-        Parameters
-        ----------
-        params: dict
-            Dictionary of user defined parameters.
+    def to_dict(self):
+        """
+        Convert the object to a dictionary, excluding attributes that start
+        with an underscore.
 
         Returns
         -------
-        ignition_locations: str
-            Data string to write to the QUIC_fire.inp input file.
-        """
-        # Get data from params
-        wind_direction = params["wind_direction"]
-        nx, ny = params["nx"], params["ny"]
-        dx, dy = params["dx"], params["dy"]
-
-        # Hold number ignitions constant at 100 per documentation
-        # num_ignition_cells = 100
-
-        # Compute cardinal direction from wind direction
-        dirs = ["N", "E", "S", "W"]
-        ix = round(wind_direction / (360. / len(dirs)))
-        cardinal_direction = dirs[ix % len(dirs)]
-
-        # Ignition strip on top border
-        if cardinal_direction == "N":
-            fire_source_x0 = int(0.1 * nx * dx)
-            fire_source_y0 = int(0.9 * ny * dy) - 1
-            fire_source_xlen = int(0.8 * nx * dx)
-            fire_source_ylen = 1
-
-        # Ignition strip on left border
-        elif cardinal_direction == "W":
-            fire_source_x0 = int(0.1 * nx * dx) - 1
-            fire_source_y0 = int(0.1 * ny * dy)
-            fire_source_xlen = 1
-            fire_source_ylen = int(0.8 * ny * dy)
-
-        # Ignition strip on bottom border
-        elif cardinal_direction == "S":
-            fire_source_x0 = int(0.1 * nx * dx)
-            fire_source_y0 = int(0.1 * ny * dy) - 1
-            fire_source_xlen = int(0.8 * nx * dx)
-            fire_source_ylen = 1
-
-        # Ignition strip on right border
+        dict
+            Dictionary representation of the object.
+        """
+        return self.model_dump(
+            exclude={"name", "_extension", "_filename", "param_info"}
+        )
+
+    def to_file(self, directory: Path, version: str = "latest"):
+        """
+        Write the input file to a specified directory.
+
+        Attributes
+        ----------
+        directory : Path
+            Directory to write the input file to.
+        version : str
+            Version of the input file to write. Default is "latest".
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        template_file_path = TEMPLATES_PATH / version / f"{self._filename}"
+        with open(template_file_path, "r") as ftemp:
+            src = Template(ftemp.read())
+
+        result = src.substitute(self.to_dict())
+
+        output_file_path = directory / self._filename
+        with open(output_file_path, "w") as fout:
+            fout.write(result)
+
+    @classmethod
+    def from_dict(cls, data: dict):
+        return cls(**data)
+
+
+class Gridlist(InputFile):
+    """
+    Class representing the gridlist.txt file. This file contains the grid
+    information for the QUIC-Fire simulation when canopies are present.
+
+    Attributes
+    ----------
+    n : int
+        Number of cells in the x-direction [-]
+    m : int
+        Number of cells in the y-direction [-]
+    l : int
+        Number of cells in the z-direction [-]
+    dx : float
+        Cell size in the x-direction [m]
+    dy : float
+        Cell size in the y-direction [m]
+    dz : float
+        Cell size in the z-direction [m]
+    aa1 : float
+        Stretching factor for the vertical grid spacing [-]
+    """
+
+    name: str = Field("gridlist", frozen=True)
+    _extension: str = ""
+    n: PositiveInt
+    m: PositiveInt
+    l: PositiveInt
+    dx: PositiveFloat
+    dy: PositiveFloat
+    dz: PositiveFloat
+    aa1: PositiveFloat
+
+
+class RasterOrigin(InputFile):
+    """
+    Class representing the rasterorigin.txt file. This file contains the
+    coordinates of the south-west corner of the domain in UTM coordinates.
+
+    Attributes
+    ----------
+    utm_x : float
+        UTM-x coordinates of the south-west corner of domain [m]
+    utm_y : float
+        UTM-y coordinates of the south-west corner of domain [m]
+    """
+
+    name: str = Field("rasterorigin", frozen=True)
+    _extension: str = ".txt"
+    utm_x: NonNegativeFloat = 0.0
+    utm_y: NonNegativeFloat = 0.0
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a RasterOrigin object from a directory containing a
+        rasterorigin.txt file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "rasterorigin.txt", "r") as f:
+            lines = f.readlines()
+        return cls(utm_x=float(lines[0].split()[0]), utm_y=float(lines[1].split()[0]))
+
+
+class QU_Buildings(InputFile):
+    """
+    Class representing the QU_buildings.inp file. This file contains the
+    building-related data for the QUIC-Fire simulation. This class is not
+    currently used in QUIC-Fire.
+
+    Attributes
+    ----------
+    wall_roughness_length : float
+        Wall roughness length [m]. Must be greater than 0. Default is 0.1.
+    number_of_buildings : int
+        Number of buildings [-]. Default is 0. Not currently used in QUIC-Fire.
+    number_of_polygon_nodes : int
+        Number of polygon building nodes [-]. Default is 0. Not currently used
+        in QUIC-Fire.
+    """
+
+    name: str = Field("QU_buildings", frozen=True)
+    _extension: str = ".inp"
+    wall_roughness_length: PositiveFloat = 0.1
+    number_of_buildings: NonNegativeInt = 0
+    number_of_polygon_nodes: NonNegativeInt = 0
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QU_Buildings object from a directory containing a
+        QU_buildings.inp file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QU_buildings.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            wall_roughness_length=float(lines[1].split()[0]),
+            number_of_buildings=int(lines[2].split()[0]),
+            number_of_polygon_nodes=int(lines[3].split()[0]),
+        )
+
+
+class QU_Fileoptions(InputFile):
+    """
+    Class representing the QU_fileoptions.inp file. This file contains
+    file output-related options for the QUIC-Fire simulation.
+
+    Attributes
+    ----------
+    output_data_file_format_flag : int
+        Output data file format flag. Values accepted are [1, 2, 3].
+        Recommended value 2. 1 - binary, 2 - ASCII, 3 - both.
+    non_mass_conserved_initial_field_flag : int
+        Flag to write out non-mass conserved initial field file uofield.dat.
+        Values accepted are [0, 1]. Recommended value 0. 0 - off, 1 - on.
+    initial_sensor_velocity_field_flag : int
+        Flag to write out the file uosensorfield.dat. Values accepted are
+        [0, 1]. Recommended value 0. 0 - off, 1 - on.
+    qu_staggered_velocity_file_flag : int
+        Flag to write out the file QU_staggered_velocity.bin. Values accepted
+        are [0, 1]. Recommended value 0. 0 - off, 1 - on.
+    generate_wind_startup_files_flag : int
+        Generate wind startup files for ensemble simulations. Values accepted
+        are [0, 1]. Recommended value 0. 0 - off, 1 - on.
+    """
+
+    name: str = Field("QU_fileoptions", frozen=True)
+    _extension: str = ".inp"
+    output_data_file_format_flag: Literal[1, 2, 3] = 2
+    non_mass_conserved_initial_field_flag: Literal[0, 1] = 0
+    initial_sensor_velocity_field_flag: Literal[0, 1] = 0
+    qu_staggered_velocity_file_flag: Literal[0, 1] = 0
+    generate_wind_startup_files_flag: Literal[0, 1] = 0
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QU_Fileoptions object from a directory containing a
+        QU_fileoptions.inp file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QU_fileoptions.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            output_data_file_format_flag=int(lines[1].split()[0]),
+            non_mass_conserved_initial_field_flag=int(lines[2].split()[0]),
+            initial_sensor_velocity_field_flag=int(lines[3].split()[0]),
+            qu_staggered_velocity_file_flag=int(lines[4].split()[0]),
+            generate_wind_startup_files_flag=int(lines[5].split()[0]),
+        )
+
+
+class QU_Simparams(InputFile):
+    """
+    Class representing the QU_simparams.inp file. This file contains the
+    simulation parameters for the QUIC-Fire simulation.
+
+    nx : int
+        Number of cells in the x-direction [-]. Recommended value: > 100
+    ny : int
+        Number of cells in the y-direction [-]. Recommended value: > 100
+    nz : int
+        Number of cells in the z-direction [-].
+    dx : float
+        Cell size in the x-direction [m]. Recommended value: 2 m
+    dy : float
+        Cell size in the y-direction [m]. Recommended value: 2 m
+    quic_domain_height : float
+        QUIC domain height [m]. Recommended value: 300 m
+    surface_vertical_cell_size : float
+        Surface vertical cell size [m].
+    number_surface_cells : int
+        Number of uniform surface cells [-].
+    stretch_grid_flag : int
+        Vertical grid stretching flag, values accepted [0, 1, 3]. Default is 3.
+        0 - uniform, 1 - custom, 3 - parabolic. If stretch_grid_flag is 0 or 3
+        dz_array is computed from nz, surface_vertical_cell_size, and
+        number_surface_cells. If stretch_grid_flag is 1, custom_dz_array must
+        be provided.
+    custom_dz_array : list[float]
+        Vertical grid spacing array [m]. Must be provided if stretch_grid_flag
+        is 1. If stretch_grid_flag is 0 or 3, dz_array is computed from nz,
+        surface_vertical_cell_size, and number_surface_cells.
+    utc_offset : int
+        Hours difference from UTC (aka UTM) [h]
+    wind_times : list[int]
+        List of times at which the winds are available in Unix Epoch time
+        (integer seconds since 1970/1/1 00:00:00). These are UTC times.
+        Defaults to [int(time.time())] if not provided.
+    sor_iter_max : int
+        Maximum number of iterations of the SOR wind solver. Recommended value:
+        10. Default is 10.
+    sor_residual_reduction : int
+        Residual reduction to assess convergence of the SOR solver (orders of
+        magnitude). Recommended value: 3. Default is 3.
+    use_diffusion_flag : int
+        Use diffusion algorithm: 0 = off, 1 = on. Recommended value: 0.
+        Default is 0.
+    number_diffusion_iterations : int
+        Number of diffusion iterations. Recommended value: 10. Default is 10.
+    domain_rotation : float
+        Domain rotation relative to true north (clockwise is positive)
+        [degrees]. Recommended value: 0 deg. Default is 0.
+    utm_x : float
+        UTM-x coordinates of the south-west corner of domain [m]. Default is 0.
+    utm_y : float
+        UTM-y coordinates of the south-west corner of domain [m]. Default is 0.
+    utm_zone_number : int
+        UTM zone number [-]. Default is 1.
+    utm_zone_letter : int
+        UTM zone letter (A=1, B=2, ...) [-]. Default is 1.
+    quic_cfd_flag : int
+        QUIC-CFD flag: 0 = off, 1 = on. Recommended value: 0. Default is 0.
+    explosive_bldg_flag : int
+        Explosive building damage flag: 0 = off, 1 = on. Recommended value: 0.
+        Default is 0.
+    bldg_array_flag : int
+        Building array flag. 0 = off, 1 = on. Recommended value: 0. Default is
+        0.
+    """
+
+    name: str = Field("QU_simparams", frozen=True)
+    _extension: str = ".inp"
+    nx: PositiveInt
+    ny: PositiveInt
+    nz: PositiveInt
+    dx: PositiveFloat
+    dy: PositiveFloat
+    quic_domain_height: PositiveFloat
+    surface_vertical_cell_size: PositiveFloat = 1.0
+    number_surface_cells: PositiveInt = 5
+    stretch_grid_flag: Literal[0, 1, 3] = 3
+    custom_dz_array: list[PositiveFloat] = []
+    utc_offset: int = 0
+    wind_times: list[int] = [int(time.time())]
+    sor_iter_max: PositiveInt = 10
+    sor_residual_reduction: PositiveInt = 3
+    use_diffusion_flag: Literal[0, 1] = 0
+    number_diffusion_iterations: PositiveInt = 10
+    domain_rotation: float = 0.0
+    utm_x: float = 0.0
+    utm_y: float = 0.0
+    utm_zone_number: PositiveInt = 1
+    utm_zone_letter: PositiveInt = 1
+    quic_cfd_flag: Literal[0, 1] = 0
+    explosive_bldg_flag: Literal[0, 1] = 0
+    bldg_array_flag: Literal[0, 1] = 0
+    _from_file: bool = False
+    _from_file_dz_array: list[PositiveFloat] = []
+
+    @computed_field
+    @property
+    def dz_array(self) -> list[float]:
+        if self._from_file:
+            return self._from_file_dz_array
+        elif self.stretch_grid_flag == 0:
+            return [self.surface_vertical_cell_size] * self.nz
+        elif self.stretch_grid_flag == 1:
+            return self.custom_dz_array
+        elif self.stretch_grid_flag == 3:
+            return compute_parabolic_stretched_grid(
+                self.surface_vertical_cell_size,
+                self.number_surface_cells,
+                self.nz,
+                self.quic_domain_height,
+            ).tolist()
+
+    @computed_field
+    @property
+    def vertical_grid_lines(self) -> str:
+        """
+        Parses the vertical grid stretching flag and dz_array to generate the
+        vertical grid as a string for the QU_simparams.inp file.
+
+        Also modifies dz_array if stretch_grid_flag is not 1.
+        """
+        stretch_grid_func_map = {
+            0: self._stretch_grid_flag_0,
+            1: self._stretch_grid_flag_1,
+            3: self._stretch_grid_flag_3,
+        }
+        return stretch_grid_func_map[self.stretch_grid_flag]()
+
+    @computed_field
+    @property
+    def wind_time_lines(self) -> str:
+        return self._generate_wind_time_lines()
+
+    def _stretch_grid_flag_0(self):
+        """
+        Generates a uniform vertical grid as a string for the QU_simparams.inp
+        file. Adds the uniform grid to dz_array.
+        """
+        # Create the lines for the uniform grid
+        surface_dz_line = (
+            f"{float(self.surface_vertical_cell_size)}\t" f"! Surface DZ [m]"
+        )
+        number_surface_cells_line = (
+            f"{self.number_surface_cells}\t" f"! Number of uniform surface cells"
+        )
+
+        return f"{surface_dz_line}\n{number_surface_cells_line}"
+
+    def _stretch_grid_flag_1(self):
+        """
+        Generates a custom vertical grid as a string for the QU_simparams.inp
+        file.
+        """
+        # Verify that dz_array is not empty
+        if not self.dz_array:
+            raise ValueError(
+                "dz_array must not be empty if stretch_grid_flag "
+                "is 1. Please provide a custom_dz_array with nz "
+                "elements or use a different stretch_grid_flag."
+            )
+
+        # Verify that nz is equal to the length of dz_array
+        if self.nz != len(self.dz_array):
+            raise ValueError(
+                f"nz must be equal to the length of dz_array. "
+                f"{self.nz} != {len(self.dz_array)}"
+            )
+
+        # Verify that the first number_surface_cells_line elements of dz_array
+        # are equal to the surface_vertical_cell_size
+        for dz in self.dz_array[: self.number_surface_cells]:
+            if dz != self.surface_vertical_cell_size:
+                raise ValueError(
+                    "The first number_surface_cells_line "
+                    "elements of dz_array must be equal to "
+                    "surface_vertical_cell_size"
+                )
+
+        # Write surface vertical cell size line
+        surface_dz_line = (
+            f"{float(self.surface_vertical_cell_size)}\t! " f"Surface DZ [m]"
+        )
+
+        # Write header line
+        header_line = "! DZ array [m]"
+
+        # Write dz_array lines
+        dz_array_lines_list = []
+        for dz in self.dz_array:
+            dz_array_lines_list.append(f"{float(dz)}")
+        dz_array_lines = "\n".join(dz_array_lines_list)
+
+        return f"{surface_dz_line}\n{header_line}\n{dz_array_lines}"
+
+    def _stretch_grid_flag_3(self):
+        """
+        Generates a vertical grid for stretch_grid_flag 3 as a string for the
+        QU_simparams.inp file. Stretch grid flag 3 is a stretching with
+        parabolic vertical cell size. Adds the parabolic grid to dz_array.
+        """
+        # Write surface vertical cell size line
+        surface_dz_line = (
+            f"{float(self.surface_vertical_cell_size)}\t! " f"Surface DZ [m]"
+        )
+
+        # Write number of surface cells line
+        number_surface_cells_line = (
+            f"{self.number_surface_cells}\t! " f"Number of uniform surface cells"
+        )
+
+        # Write header line
+        header_line = "! DZ array [m]"
+
+        # Write dz_array lines
+        dz_lines = "\n".join([f"{float(dz)}" for dz in self.dz_array])
+
+        return (
+            f"{surface_dz_line}\n{number_surface_cells_line}\n{header_line}"
+            f"\n{dz_lines}"
+        )
+
+    def _generate_wind_time_lines(self):
+        """
+        Parses the utc_offset and wind_step_times to generate the wind times
+        as a string for the QU_simparams.inp file.
+        """
+        # Verify that wind_step_times is not empty
+        if not self.wind_times:
+            raise ValueError(
+                "wind_step_times must not be empty. Please "
+                "provide a wind_step_times with num_wind_steps "
+                "elements or use a different num_wind_steps."
+            )
+
+        # Write number of time increments line
+        number_time_increments_line = (
+            f"{len(self.wind_times)}\t" f"! Number of time increments"
+        )
+
+        # Write utc_offset line
+        utc_offset_line = f"{self.utc_offset}\t! UTC offset [hours]"
+
+        # Write header line
+        header_line = "! Wind step times [s]"
+
+        # Write wind_step_times lines
+        wind_step_times_lines_list = []
+        for wind_time in self.wind_times:
+            wind_step_times_lines_list.append(f"{wind_time}")
+        wind_step_times_lines = "\n".join(wind_step_times_lines_list)
+
+        return "\n".join(
+            [
+                number_time_increments_line,
+                utc_offset_line,
+                header_line,
+                wind_step_times_lines,
+            ]
+        )
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QU_Simparams object from a directory containing a
+        QU_simparams.inp file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        with open(directory / "QU_simparams.inp", "r") as f:
+            lines = f.readlines()
+
+        # Read QU grid parameters
+        nx = int(lines[1].strip().split("!")[0])
+        ny = int(lines[2].strip().split("!")[0])
+        nz = int(lines[3].strip().split("!")[0])
+        dx = float(lines[4].strip().split("!")[0])
+        dy = float(lines[5].strip().split("!")[0])
+
+        # Read stretch grid flag
+        stretch_grid_flag = int(lines[6].strip().split("!")[0])
+
+        # Read vertical grid lines as function of stretch grid flag
+        _from_file_dz_array = []
+        custom_dz_array = []
+        if stretch_grid_flag == 0:
+            surface_vertical_cell_size = float(lines[7].strip().split("!")[0])
+            number_surface_cells = int(lines[8].strip().split("!")[0])
+            current_line = 9
+        elif stretch_grid_flag == 1:
+            surface_vertical_cell_size = float(lines[7].strip().split("!")[0])
+            number_surface_cells = 5
+            for i in range(9, 9 + nz):
+                custom_dz_array.append(float(lines[i].strip().split("!")[0]))
+            current_line = 9 + nz
+        elif stretch_grid_flag == 3:
+            surface_vertical_cell_size = float(lines[7].strip().split("!")[0])
+            number_surface_cells = int(lines[8].strip().split("!")[0])
+            _ = lines[9].strip().split("!")[0]
+            for i in range(10, 10 + nz):
+                _from_file_dz_array.append(float(lines[i].strip().split("!")[0]))
+            current_line = 10 + nz
         else:
-            fire_source_x0 = int(0.9 * nx * dx) - 1
-            fire_source_y0 = int(0.1 * ny * dy)
-            fire_source_xlen = 1
-            fire_source_ylen = int(0.8 * ny * dy)
+            raise ValueError("stretch_grid_flag must be 0, 1, or 3.")
+
+        # Read QU wind parameters
+        number_wind_steps = int(lines[current_line].strip().split("!")[0])
+        utc_offset = int(lines[current_line + 1].strip().split("!")[0])
+        _ = lines[current_line + 2].strip().split("!")[0]
+        wind_times = []
+        for i in range(current_line + 3, current_line + 3 + number_wind_steps):
+            wind_times.append(int(lines[i].strip()))
+        current_line = current_line + 3 + number_wind_steps
+
+        # Skip not used parameters
+        current_line += 9
+
+        # Read remaining QU parameters
+        sor_iter_max = int(lines[current_line].strip().split("!")[0])
+        sor_residual_reduction = int(lines[current_line + 1].strip().split("!")[0])
+        use_diffusion_flag = int(lines[current_line + 2].strip().split("!")[0])
+        number_diffusion_iterations = int(lines[current_line + 3].strip().split("!")[0])
+        domain_rotation = float(lines[current_line + 4].strip().split("!")[0])
+        utm_x = float(lines[current_line + 5].strip().split("!")[0])
+        utm_y = float(lines[current_line + 6].strip().split("!")[0])
+        utm_zone_number = int(lines[current_line + 7].strip().split("!")[0])
+        utm_zone_letter = int(lines[current_line + 8].strip().split("!")[0])
+        quic_cfd_flag = int(lines[current_line + 9].strip().split("!")[0])
+        explosive_bldg_flag = int(lines[current_line + 10].strip().split("!")[0])
+        bldg_array_flag = int(lines[current_line + 11].strip().split("!")[0])
+
+        return cls(
+            nx=nx,
+            ny=ny,
+            nz=nz,
+            dx=dx,
+            dy=dy,
+            surface_vertical_cell_size=surface_vertical_cell_size,
+            number_surface_cells=number_surface_cells,
+            stretch_grid_flag=stretch_grid_flag,
+            custom_dz_array=custom_dz_array,
+            utc_offset=utc_offset,
+            wind_times=wind_times,
+            sor_iter_max=sor_iter_max,
+            sor_residual_reduction=sor_residual_reduction,
+            use_diffusion_flag=use_diffusion_flag,
+            number_diffusion_iterations=number_diffusion_iterations,
+            domain_rotation=domain_rotation,
+            utm_x=utm_x,
+            utm_y=utm_y,
+            utm_zone_number=utm_zone_number,
+            utm_zone_letter=utm_zone_letter,
+            quic_cfd_flag=quic_cfd_flag,
+            explosive_bldg_flag=explosive_bldg_flag,
+            bldg_array_flag=bldg_array_flag,
+            _from_file=True,
+            _from_file_dz_array=_from_file_dz_array,
+        )
 
-        ignition_list = ["", fire_source_x0, fire_source_y0, fire_source_xlen,
-                         fire_source_ylen]
-        return "\n".join((str(x) for x in ignition_list))
 
-    def _fill_form_with_dict(self, template_file_path: Path, params: dict):
+class QFire_Advanced_User_Inputs(InputFile):
+    """
+    Class representing the QFire_Advanced_User_Inputs.inp input file. This file
+    contains advanced parameters related to firebrands.
+
+    Attributes
+    ----------
+    fraction_cells_launch_firebrands : PositiveFloat
+        Fraction of cells that could launch firebrand tracers from which
+        firebrand tracers will actually be launched [-]. Higher value = more
+        firebrand tracers. Recommended value: 0.05
+    firebrand_radius_scale_factor : PositiveFloat
+        Multiplicative factor used to relate the length scale of the mixing
+        (firebrand distribution entrainment length scale) to the initial size
+        of the distribution [-]. Higher value = higher growth rate or RT
+        (firebrand distribution) with flight time. Recommended value: 40
+    firebrand_trajectory_time_step : PositiveInt
+        Time step used to determine the firebrand tracer trajectory [s].
+        Higher value = less accurate trajectory. Recommended value: 1 s
+    firebrand_launch_interval : PositiveInt
+        Time interval between launching of firebrand tracers [s]. Higher value =
+        less firebrand tracers launched. Recommended value: 10 s
+    firebrands_per_deposition : PositiveInt
+        Number of firebrand tracers that one firebrand tracer launched
+        represents [-]. Recommended value: 500
+    firebrand_area_ratio : PositiveFloat
+        Multiplicative factor used to relate the cell area and fraction of cells
+        from which tracers are launched to initial area represented by one
+        firebrand [-].
+    minimum_burn_rate_coefficient : PositiveFloat
+        Multiplicative factor relating the minimum mass-loss rate that a
+        firebrand tracer needs to have to justify continuing to track its
+        trajectory to the energy associated with a new ignition [-].
+    max_firebrand_thickness_fraction : PositiveFloat
+        Multiplicative factor relating the thickness of launched firebrand
+        tracer to maximum loftable firebrand thickness [-].
+    firebrand_germination_delay : PositiveInt
+        Time after a firebrand has landed at which a fire is started [s]
+    vertical_velocity_scale_factor : PositiveFloat
+        Maximum value of the multiplicative factor of the vertical velocity
+        experienced by a firebrand = 1/(fraction of the QUIC-URB on fire) [-]
+    minimum_firebrand_ignitions : PositiveInt
+        Minimum number of ignitions to be sampled in a position where a
+        firebrand lands [-]
+    maximum_firebrand_ignitions : PositiveInt
+        Maximum number of ignitions sampled at positions distributed within RT
+        around where a firebrand tracer lands [-]
+    minimum_landing_angle : PositiveFloat
+        Minimum value considered for the angle between the trajectory of the
+        firebrand when it hits the ground and horizontal [rad]
+    maximum_firebrand_thickness : PositiveFloat
+        Maximum firebrand's thickness [m]
+    """
+
+    name: str = Field("QFire_Advanced_User_Inputs", frozen=True)
+    _extension: str = ".inp"
+    fraction_cells_launch_firebrands: PositiveFloat = Field(0.05, ge=0, lt=1)
+    firebrand_radius_scale_factor: PositiveFloat = Field(40.0, ge=1)
+    firebrand_trajectory_time_step: PositiveInt = 1
+    firebrand_launch_interval: PositiveInt = 10
+    firebrands_per_deposition: PositiveInt = 500
+    firebrand_area_ratio: PositiveFloat = 20.0
+    minimum_burn_rate_coefficient: PositiveFloat = 50.0
+    max_firebrand_thickness_fraction: PositiveFloat = 0.75
+    firebrand_germination_delay: PositiveInt = 180
+    vertical_velocity_scale_factor: PositiveFloat = 5.0
+    minimum_firebrand_ignitions: PositiveInt = 50
+    maximum_firebrand_ignitions: PositiveInt = 100
+    minimum_landing_angle: PositiveFloat = Field(0.523598, ge=0, le=np.pi / 2)
+    maximum_firebrand_thickness: PositiveFloat = 0.03
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QFire_Advanced_User_Inputs object from a directory
+        containing a QFire_Advanced_User_Inputs.inp file.
         """
-        Fills a form with a dictionary of values.
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QFire_Advanced_User_Inputs.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            fraction_cells_launch_firebrands=float(lines[0].split()[0]),
+            firebrand_radius_scale_factor=float(lines[1].split()[0]),
+            firebrand_trajectory_time_step=int(lines[2].split()[0]),
+            firebrand_launch_interval=int(lines[3].split()[0]),
+            firebrands_per_deposition=int(lines[4].split()[0]),
+            firebrand_area_ratio=float(lines[5].split()[0]),
+            minimum_burn_rate_coefficient=float(lines[6].split()[0]),
+            max_firebrand_thickness_fraction=float(lines[7].split()[0]),
+            firebrand_germination_delay=int(lines[8].split()[0]),
+            vertical_velocity_scale_factor=float(lines[9].split()[0]),
+            minimum_firebrand_ignitions=int(lines[10].split()[0]),
+            maximum_firebrand_ignitions=int(lines[11].split()[0]),
+            minimum_landing_angle=float(lines[12].split()[0]),
+            maximum_firebrand_thickness=float(lines[13].split()[0]),
+        )
 
-        Parameters
-        ----------
-        template_file_path: Path
-            Path to the template file
-        params: dict
-            Dictionary of user defined parameters.
 
-        Returns
-        -------
-        None:
-            Writes parameter data to the template file
+class QUIC_fire(InputFile):
+    """
+    Class representing the QUIC_fire.inp input file. This file
+    contains the parameters relating to the fire simulation and
+    outputs.
+
+    Parameters
+    ----------
+    nz : int
+        Number of fire grid cells in the z-direction.
+    time_now : int
+        When the fire is ignited in Unix Epoch time (integer seconds since
+        1970/1/1 00:00:00). Must be greater or equal to the time of the first
+        wind
+    sim_time : int
+        Total simulation time for the fire [s]
+    out_time_fire : PositiveInt
+        After how many fire time steps to print out fire-related files (excluding emissions and radiation)
+    out_time_wind : PositiveInt
+        After how many quic updates to print out wind-related files
+    out_time_emis_rad : PositiveInt
+        After how many fire time steps to average emissions and radiation
+    out_time_wind_avg : PositiveInt
+        After how many quic updates to print out averaged wind-related files
+    fire_flag : int
+        Fire flag, 1 = run fire; 0 = no fire
+    random_seed : int
+        Random number generator, -1: use time and date, any other integer > 0
+        is used as the seed
+    fire_time_step : int
+        time step for the fire simulation [s]
+    quic_time_step : int
+        Number of fire time steps done before updating the quic wind field
+        (integer, >= 1)
+    stretch_grid_flag : int
+        Vertical stretching flag: 0 = uniform dz, 1 = custom
+    file_path : str
+        Path to files defining fuels, ignitions, and topography, with file
+        separator at the end. Defaults to "", indicating files are in the same directory as all other input files
+    dz : int
+        Cell size in the z-direction [m] of the fire grid. Recommended value: 1m
+    dz_array : list[float]
+        custom dz, one dz per line must be specified, from the ground to the
+        top of the domain
+    fuel_flag : int
+        Flag for fuel inputs:
+            - density
+            - moisture
+            - height
+        1 = uniform; 2 = provided thru QF_FuelDensity.inp, 3 = Firetech files
+        for quic grid, 4 = Firetech files for different grid
+        (need interpolation)
+    fuel_density : PositiveFloat
+        Fuel density (kg/m3)
+    fuel_moisture : PositiveFloat
+        Fuel moisture = mass of water/mass of dry fuel
+    fuel_height : PositiveFloat
+        Fuel height of surface layer (m)
+    ignition_type: IgnitionType
+        Ignitions shape or source. See ignitions module.
+    ignitions_per_cell: int
+        Number of ignition per cell of the fire model. Recommended max value
+        of 100
+    firebrand_flag : int
+        Firebrand flag, 0 = off; 1 = on. Recommended value = 0 ; firebrands
+        are untested for small scale problems
+    auto_kill : int
+        Kill if the fire is out and there are no more ignitions or firebrands
+        (0 = no, 1 = yes)
+    eng_to_atm_out : int
+        Output flag [0, 1]: gridded energy-to-atmosphere
+        (3D fire grid + extra layers)
+    react_rate_out : int
+        Output flag [0, 1]: compressed array reaction rate (fire grid)
+    fuel_dens_out : int
+        Output flag [0, 1]: compressed array fuel density (fire grid)
+    QF_wind_out : int
+        Output flag [0, 1]: gridded wind (u,v,w,sigma) (3D fire grid)
+    QU_wind_inst_out : int
+        Output flag [0, 1]: gridded QU winds with fire effects, instantaneous
+        (QUIC-URB grid)
+    QU_wind_avg_out : int
+        Output flag [0, 1]: gridded QU winds with fire effects, averaged
+        (QUIC-URB grid)
+    fuel_moist_out : int
+        Output flag [0, 1]: compressed array fuel moisture (fire grid)
+    mass_burnt_out : int
+        Output flag [0, 1]: vertically-integrated % mass burnt (fire grid)
+    firebrand_out : int
+        Output flag [0, 1]: firebrand trajectories. Must be 0 when firebrand
+        flag is 0
+    emissions_out : int
+        Output flag [0, 5]: compressed array emissions (fire grid):
+            0 = do not output any emission related variables
+            1 = output emissions files and simulate CO in QUIC-SMOKE
+            2 = output emissions files and simulate PM2.5 in QUIC- SMOKE
+            3 = output emissions files and simulate both CO and PM2.5 in
+                QUIC-SMOKE
+            4 = output emissions files but use library approach in QUIC-SMOKE
+            5 = output emissions files and simulate both water in QUIC-SMOKE
+    radiation_out : int
+        Output flag [0, 1]: gridded thermal radiation (fire grid)
+    intensity_out : int
+        Output flag [0, 1]: surface fire intensity at every fire time step
+    """
+
+    name: str = "QUIC_fire"
+    _extension: str = ".inp"
+    nz: PositiveInt
+    time_now: PositiveInt
+    sim_time: PositiveInt
+    out_time_fire: PositiveInt = 30
+    out_time_wind: PositiveInt = 30
+    out_time_emis_rad: PositiveInt = 30
+    out_time_wind_avg: PositiveInt = 30
+    ignition_type: SerializeAsAny[IgnitionType] = IgnitionType(ignition_flag=6)
+    fire_flag: Literal[0, 1] = 1
+    random_seed: int = Field(ge=-1, default=-1)
+    fire_time_step: PositiveInt = 1
+    quic_time_step: PositiveInt = 1
+    stretch_grid_flag: Literal[0, 1] = 0
+    dz: PositiveInt = 1
+    dz_array: list[PositiveFloat] = []
+    fuel_flag: Literal[1, 2, 3, 4] = 3
+    fuel_density: PositiveFloat | None = None
+    fuel_moisture: PositiveFloat | None = None
+    fuel_height: PositiveFloat | None = None
+    ignitions_per_cell: PositiveInt = 2
+    firebrand_flag: Literal[0, 1] = 0
+    auto_kill: Literal[0, 1] = 1
+    # Output flags
+    eng_to_atm_out: Literal[0, 1] = 1
+    react_rate_out: Literal[0, 1] = 0
+    fuel_dens_out: Literal[0, 1] = 1
+    QF_wind_out: Literal[0, 1] = 1
+    QU_wind_inst_out: Literal[0, 1] = 1
+    QU_wind_avg_out: Literal[0, 1] = 0
+    fuel_moist_out: Literal[0, 1] = 1
+    mass_burnt_out: Literal[0, 1] = 1
+    firebrand_out: Literal[0, 1] = 0
+    emissions_out: Literal[0, 1, 2, 3, 4, 5] = 0
+    radiation_out: Literal[0, 1] = 0
+    intensity_out: Literal[0, 1] = 0
+
+    @field_validator("random_seed")
+    @classmethod
+    def validate_random_seed(cls, v: int) -> int:
+        if v == 0:
+            raise ValueError("QUIC_fire.inp: random_seed must be not be 0")
+        return v
+
+    @computed_field
+    @property
+    def stretch_grid_input(self) -> str:
         """
-        output_file_path = self.directory / template_file_path.name
-        with open(template_file_path, "r") as ftemp:
-            with open(output_file_path, "w") as fout:
-                src = Template(ftemp.read())
-                result = src.substitute(params)
-                fout.write(result)
-
-
-if __name__ == '__main__':
-    test_params = SimulationParameters(
-        nx=100,
-        ny=100,
-        nz=1,
-        dx=1.,
-        dy=1.,
-        dz=1.,
-        wind_speed=4.,
-        wind_direction=270,
-        sim_time=60,
-        auto_kill=1,
-        num_cpus=1,
-        fuel_flag=3,
-        ignition_flag=1,
-        output_time=10,
-        topo_flag=0,
-    )
-
-    sim_test = SimulationInputs("../tests/tmp/test-simulation/")
-    sim_test.setup_input_files(test_params)
+        Writes a custom stretch grid to QUIC_fire.inp, if provided.
+        """
+        if self.stretch_grid_flag == 1:
+            # Verify that dz_array is not empty
+            if not self.dz_array:
+                raise ValueError(
+                    "dz_array must not be empty if stretch_grid_flag "
+                    "is 1. Please provide a dz_array with nz elements"
+                    " or use a different stretch_grid_flag."
+                )
+
+            # Verify that nz is equal to the length of dz_array
+            if self.nz != len(self.dz_array):
+                raise ValueError(
+                    f"nz must be equal to the length of dz_array. "
+                    f"{self.nz} != {len(self.dz_array)}"
+                )
+
+            # Write dz_array lines
+            dz_array_lines_list = []
+            for dz in self.dz_array:
+                dz_array_lines_list.append(f"{float(dz)}")
+            dz_array_lines = "\n".join(dz_array_lines_list)
+
+            return f"{dz_array_lines}\n"
+        else:
+            return str(self.dz)
+
+    @computed_field
+    @property
+    def ignition_lines(self) -> str:
+        return str(self.ignition_type)
+
+    @computed_field
+    @property
+    def fuel_lines(self) -> str:
+        flag_line = (
+            " 1 = uniform; "
+            "2 = provided thru QF_FuelMoisture.inp, 3 = Firetech"
+            " files for quic grid, 4 = Firetech files for "
+            "different grid (need interpolation)"
+        )
+        fuel_density_flag_line = f"{self.fuel_flag}\t! fuel density flag:" + flag_line
+        fuel_moist_flag_line = f"\n{self.fuel_flag}\t! fuel moisture flag:" + flag_line
+        fuel_height_flag_line = f"\n{self.fuel_flag}\t! fuel height flag:" + flag_line
+        if self.fuel_flag == 1:
+            try:
+                assert self.fuel_density is not None
+                assert self.fuel_moisture is not None
+                assert self.fuel_height is not None
+            except AssertionError:
+                raise ValueError(
+                    "fuel_params: FuelInputs class must have values for fuel_density, fuel_moisture, and fuel_height"
+                )
+            fuel_dens_line = f"\n{self.fuel_density}"
+            fuel_moist_line = f"\n{self.fuel_moisture}"
+            fuel_height_line = f"\n{self.fuel_height}"
+            return (
+                fuel_density_flag_line
+                + fuel_dens_line
+                + fuel_moist_flag_line
+                + fuel_moist_line
+                + fuel_height_flag_line
+                + fuel_height_line
+            )
+        return fuel_density_flag_line + fuel_moist_flag_line
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QUIC_fire object from a directory containing a
+        QUIC_Fire.inp file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        with open(directory / "QUIC_fire.inp", "r") as f:
+            lines = f.readlines()
+
+        # Read fire flag and random seed
+        fire_flag = int(lines[0].strip().split("!")[0])
+        random_seed = int(lines[1].strip().split("!")[0])
+
+        # Read fire times
+        time_now = int(lines[3].strip().split("!")[0])
+        sim_time = int(lines[4].strip().split("!")[0])
+        fire_time_step = int(lines[5].strip().split("!")[0])
+        quic_time_step = int(lines[6].strip().split("!")[0])
+        out_time_fire = int(lines[7].strip().split("!")[0])
+        out_time_wind = int(lines[8].strip().split("!")[0])
+        out_time_emis_rad = int(lines[9].strip().split("!")[0])
+        out_time_wind_avg = int(lines[10].strip().split("!")[0])
+
+        # Read fire grid parameters
+        nz = int(lines[12].strip().split("!")[0])
+        stretch_grid_flag = int(lines[13].strip().split("!")[0])
+        dz_array = []
+        if stretch_grid_flag == 0:
+            dz = int(lines[14].strip().split("!")[0])
+            current_line = 15
+        else:
+            for i in range(14, 14 + len(nz)):
+                try:
+                    float(lines[i].strip())
+                except ValueError:
+                    print(
+                        "QUIC_fire.inp: dz input value is not a float. Does the number of dz inputs match nz?"
+                    )
+                dz_array.append(float(lines[i].strip()))
+            current_line = 15 + len(nz)
+
+        current_line += 4  # skip unused lines
+
+        # Read fuel inputs
+        # current_line = ! FUEL
+        current_line += 1  # header
+        fuel_flag = int(lines[current_line].strip().split("!")[0])
+        if fuel_flag == 1:
+            fuel_density = float(lines[current_line + 1].strip())
+            moisture_flag = int(lines[current_line + 2].strip().split("!")[0])
+            fuel_moisture = float(lines[current_line + 3].strip())
+            height_flag = int(lines[current_line + 4].strip().split("!")[0])
+            fuel_height = float(lines[current_line + 5].strip())
+            if moisture_flag != fuel_flag or height_flag != fuel_flag:
+                raise ValueError(
+                    "QUIC_fire.inp: Fuel moisture and fue height flags must match fuel density flag"
+                )
+            current_line += 6
+        else:
+            fuel_density = None
+            fuel_moisture = None
+            fuel_height = None
+            current_line += 2
+
+            # Read ignition inputs
+        # current_line = ! IGNITION LOCATIONS
+        current_line += 1  # header
+        ignition_flag = int(lines[current_line].strip().split("!")[0])
+        add_lines = {1: 4, 2: 6, 3: 5, 4: 0, 5: 0, 6: 0, 7: 0}
+        add = add_lines.get(ignition_flag)
+        ignition_params = []
+        current_line += 1
+        for i in range(current_line, current_line + add):
+            ignition_params.append(int(lines[i].strip().split("!")[0]))
+        if ignition_flag == 1:
+            x_min, y_min, x_length, y_length = ignition_params
+            ignition_type = RectangleIgnition(x_min, y_min, x_length, y_length)
+        elif ignition_flag == 2:
+            x_min, y_min, x_length, y_length, x_width, y_width = ignition_params
+            ignition_type = SquareRingIgnition(
+                x_min, y_min, x_length, y_length, x_width, y_width
+            )
+        elif ignition_flag == 3:
+            x_min, y_min, x_length, y_length, ring_width = ignition_params
+            ignition_type = CircularRingIgnition(
+                x_min, y_min, x_length, y_length, ring_width
+            )
+        elif ignition_flag == 6:
+            ignition_type = IgnitionType(ignition_flag=6)
+        current_line += add
+        ignitions_per_cell = int(lines[current_line].strip().split("!")[0])
+        current_line += 1
+
+        # Read firebrands
+        # current_line = ! FIREBRANDS
+        current_line += 1  # header
+        firebrand_flag = int(lines[current_line].strip().split("!")[0])
+        current_line += 1
+
+        # Read output flags
+        # current_line = !OUTPUT_FILES
+        eng_to_atm_out = int(lines[current_line + 1].strip().split("!")[0])
+        react_rate_out = int(lines[current_line + 2].strip().split("!")[0])
+        fuel_dens_out = int(lines[current_line + 3].strip().split("!")[0])
+        QF_wind_out = int(lines[current_line + 4].strip().split("!")[0])
+        QU_wind_inst_out = int(lines[current_line + 5].strip().split("!")[0])
+        QU_wind_avg_out = int(lines[current_line + 6].strip().split("!")[0])
+        # ! Output plume trajectories
+        fuel_moist_out = int(lines[current_line + 8].strip().split("!")[0])
+        mass_burnt_out = int(lines[current_line + 9].strip().split("!")[0])
+        firebrand_out = int(lines[current_line + 10].strip().split("!")[0])
+        emissions_out = int(lines[current_line + 11].strip().split("!")[0])
+        radiation_out = int(lines[current_line + 12].strip().split("!")[0])
+        intensity_out = int(lines[current_line + 13].strip().split("!")[0])
+        # ! AUTOKILL
+        auto_kill = int(lines[current_line + 15].strip().split("!")[0])
+
+        return cls(
+            fire_flag=fire_flag,
+            random_seed=random_seed,
+            time_now=time_now,
+            sim_time=sim_time,
+            fire_time_step=fire_time_step,
+            quic_time_step=quic_time_step,
+            out_time_fire=out_time_fire,
+            out_time_wind=out_time_wind,
+            out_time_emis_rad=out_time_emis_rad,
+            out_time_wind_avg=out_time_wind_avg,
+            nz=nz,
+            stretch_grid_flag=stretch_grid_flag,
+            dz=dz,
+            dz_array=dz_array,
+            fuel_flag=fuel_flag,
+            fuel_density=fuel_density,
+            fuel_moisture=fuel_moisture,
+            fuel_height=fuel_height,
+            ignition_type=ignition_type,
+            ignitions_per_cell=ignitions_per_cell,
+            firebrand_flag=firebrand_flag,
+            eng_to_atm_out=eng_to_atm_out,
+            react_rate_out=react_rate_out,
+            fuel_dens_out=fuel_dens_out,
+            QF_wind_out=QF_wind_out,
+            QU_wind_inst_out=QU_wind_inst_out,
+            QU_wind_avg_out=QU_wind_avg_out,
+            fuel_moist_out=fuel_moist_out,
+            mass_burnt_out=mass_burnt_out,
+            firebrand_out=firebrand_out,
+            emissions_out=emissions_out,
+            radiation_out=radiation_out,
+            intensity_out=intensity_out,
+            auto_kill=auto_kill,
+        )
+
+
+class QFire_Bldg_Advanced_User_Inputs(InputFile):
+    """
+    Class representing the QFire_Bldg_Advanced_User_Inputs.inp input file. This
+    file contains advanced parameters related to buildings and fuel.
+
+    Attributes
+    ----------
+    convert_buildings_to_fuel_flag : int
+        Flag to convert QUIC-URB buildings to fuel. 0 = do not convert,
+        1 = convert. Recommended value: 0.
+    building_fuel_density : PositiveFloat
+        Thin fuel density within buildings if no fuel is specified and buildings
+        are converted to fuel. Higher value = more fuel. Recommended value: 0.5.
+        Units: [kg/m^3]
+    building_attenuation_coefficient : PositiveFloat
+        Attenuation coefficient within buildings if buildings are converted to
+        fuel. Higher value = more drag. Recommended value: 2.
+    building_surface_roughness : PositiveFloat
+        Surface roughness within buildings if buildings are converted to fuel.
+        Higher value = lower wind speed. Recommended value: 0.01 m. Units: [m]
+    convert_fuel_to_canopy_flag : int
+        Flag to convert fuel to canopy for winds. 0 = do not convert,
+        1 = convert. Recommended value: 1.
+    update_canopy_winds_flag : int
+        Flag to update canopy winds when fuel is consumed. 0 = do not update,
+        1 = update. Recommended value: 1.
+    fuel_attenuation_coefficient : PositiveFloat
+        Attenuation coefficient within fuel for the wind profile. Higher
+        value = more drag. Recommended value: 1.
+    fuel_surface_roughness : PositiveFloat
+        Surface roughness within fuel. Higher value = lower wind speed.
+        Recommended value: 0.1 m. Units: [m]
+    """
+
+    name: str = Field("QFire_Bldg_Advanced_User_Inputs", frozen=True)
+    _extension: str = ".inp"
+    convert_buildings_to_fuel_flag: Literal[0, 1] = 0
+    building_fuel_density: PositiveFloat = Field(0.5, ge=0)
+    building_attenuation_coefficient: PositiveFloat = Field(2.0, ge=0)
+    building_surface_roughness: PositiveFloat = Field(0.01, ge=0)
+    convert_fuel_to_canopy_flag: Literal[0, 1] = 1
+    update_canopy_winds_flag: Literal[0, 1] = 1
+    fuel_attenuation_coefficient: PositiveFloat = Field(1.0, ge=0)
+    fuel_surface_roughness: PositiveFloat = Field(0.1, ge=0)
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a QFire_Bldg_Advanced_User_Inputs object from a directory
+        containing a QFire_Bldg_Advanced_User_Inputs.inp file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QFire_Bldg_Advanced_User_Inputs.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            convert_buildings_to_fuel_flag=int(lines[0].split()[0]),
+            building_fuel_density=float(lines[1].split()[0]),
+            building_attenuation_coefficient=float(lines[2].split()[0]),
+            building_surface_roughness=float(lines[3].split()[0]),
+            convert_fuel_to_canopy_flag=int(lines[4].split()[0]),
+            update_canopy_winds_flag=int(lines[5].split()[0]),
+            fuel_attenuation_coefficient=float(lines[6].split()[0]),
+            fuel_surface_roughness=float(lines[7].split()[0]),
+        )
+
+
+class QFire_Plume_Advanced_User_Inputs(InputFile):
+    """
+    Class representing the QFire_Plume_Advanced_User_Inputs.inp input file.
+    This file contains advanced parameters related to modeling buoyant plumes.
+
+    Attributes
+    ----------
+    max_plumes_per_timestep : PositiveInt
+        Maximum number of plumes allowed at each time step. Higher values slow
+        down the simulation. Default value: 150,000. Recommended range:
+        50,000 - 500,000.
+    min_plume_updraft_velocity : PositiveFloat
+        Minimum plume updraft velocity [m/s]. If plume velocity drops below this
+        value, the plume is removed. Higher values reduce number of plumes.
+        Default value: 0.1 m/s.
+    max_plume_updraft_velocity : PositiveFloat
+        Maximum allowed plume updraft velocity [m/s]. Default value: 100 m/s.
+    min_velocity_ratio : PositiveFloat
+        Minimum ratio between plume updraft velocity and wind speed. If ratio
+        drops below this value, plume is removed. Higher values reduce plumes.
+        Default value: 0.1.
+    brunt_vaisala_freq_squared : NonNegativeFloat
+        Inverse of the Brunt-Vaisala frequency squared [1/s^2], a measure of
+        atmospheric stability. Default value: 0 1/s^2.
+    creeping_flag : Literal[0, 1]
+        Flag to enable (1) or disable (0) fire spread by creeping.
+        Default value: 1.
+    adaptive_timestep_flag : Literal[0, 1]
+        Enable (1) or disable (0) adaptive time stepping. Adaptive time stepping
+        improves accuracy but increases simulation time. Default value: 0.
+    plume_timestep : PositiveFloat
+        Time step [s] used to compute buoyant plume trajectories. Higher values
+        reduce accuracy. Default value: 1s.
+    sor_option_flag : Literal[0, 1]
+        SOR solver option. 0 = standard SOR, 1 = memory SOR. Default value: 1.
+    sor_alpha_plume_center : PositiveFloat
+        SOR alpha value at plume centerline. Higher values reduce influence of
+        plumes on winds. Default value: 10.
+    sor_alpha_plume_edge : PositiveFloat
+        SOR alpha value at plume edge. Higher values reduce influence of plumes
+        on winds. Default value: 1.
+        max_plume_merging_angle : PositiveFloat
+        Maximum angle [degrees] between plumes to determine merging eligibility.
+        Higher values increase plume merging. Default value: 30 degrees.
+    max_plume_overlap_fraction : PositiveFloat
+        Maximum fraction of smaller plume trajectory overlapped by larger plume
+        to be considered for merging. Higher values increase merging.
+    plume_to_grid_updrafts_flag : Literal[0, 1]
+        Method to map plume updrafts to grid. 0 = new method, 1 = old method.
+        New method improves accuracy. Default value: 1. New method takes longer,
+        but is needed if smoke is simulated afterwards.
+    max_points_along_plume_edge : PositiveInt
+        Maximum points to sample along grid cell edge for new plume-to-grid
+        method. Default value: 10.
+    plume_to_grid_intersection_flag : Literal[0, 1]
+        Scheme to sum plume-to-grid updrafts when multiple plumes intersect a
+        grid cell. 0 = cube method, 1 = max value method. Default value: 1.
+    """
+
+    name: str = Field("QFire_Plume_Advanced_User_Inputs", frozen=True)
+    _extension: str = ".inp"
+    max_plumes_per_timestep: PositiveInt = Field(150000, gt=0)
+    min_plume_updraft_velocity: PositiveFloat = Field(0.1, gt=0)
+    max_plume_updraft_velocity: PositiveFloat = Field(100.0, gt=0)
+    min_velocity_ratio: PositiveFloat = Field(0.1, gt=0)
+    brunt_vaisala_freq_squared: NonNegativeFloat = Field(0.0, ge=0)
+    creeping_flag: Literal[0, 1] = 1
+    adaptive_timestep_flag: Literal[0, 1] = 0
+    plume_timestep: PositiveFloat = Field(1.0, gt=0)
+    sor_option_flag: Literal[0, 1] = 1
+    sor_alpha_plume_center: PositiveFloat = Field(10.0, gt=0)
+    sor_alpha_plume_edge: PositiveFloat = Field(1.0, gt=0)
+    max_plume_merging_angle: PositiveFloat = Field(30.0, gt=0, le=180)
+    max_plume_overlap_fraction: PositiveFloat = Field(0.7, gt=0, le=1)
+    plume_to_grid_updrafts_flag: Literal[0, 1] = 1
+    max_points_along_plume_edge: PositiveInt = Field(10, ge=1, le=100)
+    plume_to_grid_intersection_flag: Literal[0, 1] = 1
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        with open(directory / "QFire_Plume_Advanced_User_Inputs.inp", "r") as f:
+            lines = f.readlines()
+
+        return cls(
+            max_plumes_per_timestep=int(lines[0].split()[0]),
+            min_plume_updraft_velocity=float(lines[1].split()[0]),
+            max_plume_updraft_velocity=float(lines[2].split()[0]),
+            min_velocity_ratio=float(lines[3].split()[0]),
+            brunt_vaisala_freq_squared=float(lines[4].split()[0]),
+            creeping_flag=int(lines[5].split()[0]),
+            adaptive_timestep_flag=int(lines[6].split()[0]),
+            plume_timestep=float(lines[7].split()[0]),
+            sor_option_flag=int(lines[8].split()[0]),
+            sor_alpha_plume_center=float(lines[9].split()[0]),
+            sor_alpha_plume_edge=float(lines[10].split()[0]),
+            max_plume_merging_angle=float(lines[11].split()[0]),
+            max_plume_overlap_fraction=float(lines[12].split()[0]),
+            plume_to_grid_updrafts_flag=int(lines[13].split()[0]),
+            max_points_along_plume_edge=int(lines[14].split()[0]),
+            plume_to_grid_intersection_flag=int(lines[15].split()[0]),
+        )
+
+
+class QU_TopoInputs(InputFile):
+    """
+    Class representing the QU_TopoInputs.inp input file. This file
+    contains advanced inputs pertaining to topography.
+
+    filename : str
+        Path to the custom topo file (only used with option 5). Cannot be .bin. Use .dat or .inp
+    topo_type : TopoType
+        Topography type specified as a TopoType class from topography.py
+        0 = no terrain file provided, QUIC-Fire is run with flat terrain
+        1 = Gaussian hill
+        2 = hill pass
+        3 = slope mesa
+        4 = canyon
+        5 = custom
+        6 = half circle
+        7 = sinusoid
+        8 = cos hill
+        9 = terrain is provided via QP_elevation.bin (see Section 2.7)
+        10 = terrain is provided via terrainOutput.txt
+        11 = terrain.dat (firetec)
+    smoothing_method : int
+        0 = none (default for idealized topo)
+        1 = Blur
+        2 = David Robinson’s method based on second derivative
+    smoothing_passes : int
+        Number of smoothing passes. Real terrain MUST be smoothed
+    sor_iterations : int
+        Number of SOR iteration to define background winds before starting the fire
+    sor_cycles : int
+        Number of times the SOR solver initial fields is reset to define
+        background winds before starting the fire
+    sor_relax : float
+        SOR overrelaxation coefficient. Only used if there is topo.
+    """
+
+    name: str = "QU_TopoInputs"
+    _extension: str = ".inp"
+    filename: str = "topo.dat"
+    topo_type: SerializeAsAny[TopoType] = TopoType(topo_flag=0)
+    smoothing_method: Literal[0, 1, 2] = 2
+    smoothing_passes: PositiveInt = Field(le=500, default=500)
+    sor_iterations: PositiveInt = Field(le=500, default=200)
+    sor_cycles: Literal[0, 1, 2, 3, 4] = 4
+    sor_relax: PositiveFloat = Field(le=2, default=0.9)
+
+    @computed_field
+    @property
+    def topo_lines(self) -> str:
+        return str(self.topo_type)
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QU_TopoInputs.inp", "r") as f:
+            lines = f.readlines()
+
+        # Line 0 is Header
+        filename = str(lines[1].strip())
+        # Get topo lines
+        topo_flag = int(lines[2].strip().split("!")[0])
+        add_dict = {
+            0: 0,
+            1: 4,
+            2: 2,
+            3: 3,
+            4: 5,
+            5: 0,
+            6: 3,
+            7: 2,
+            8: 2,
+            9: 0,
+            10: 0,
+            11: 0,
+        }
+        add = add_dict.get(topo_flag)
+        topo_params = []
+        for i in range(3, 3 + add):
+            topo_params.append(float(lines[i].strip().split("!")[0]))
+        if topo_flag == 1:
+            x_hilltop, y_hilltop, elevation_max, elevation_std = topo_params
+            topo_type = GaussianHillTopo(
+                x_hilltop=int(x_hilltop),
+                y_hilltop=int(y_hilltop),
+                elevation_max=int(elevation_max),
+                elevation_std=elevation_std,
+            )
+        elif topo_flag == 2:
+            max_height, location_param = topo_params
+            topo_type = HillPassTopo(
+                max_height=int(max_height), location_param=location_param
+            )
+        elif topo_flag == 3:
+            slope_axis, slope_value, flat_fraction = topo_params
+            topo_type = SlopeMesaTopo(
+                slope_axis=int(slope_axis),
+                slope_value=slope_value,
+                flat_fraction=flat_fraction,
+            )
+        elif topo_flag == 4:
+            x_start, y_center, slope_value, canyon_std, vertical_offset = topo_params
+            topo_type = CanyonTopo(
+                x_start=int(x_start),
+                y_center=int(y_center),
+                sloe_value=slope_value,
+                canyon_std=canyon_std,
+                vertical_offset=vertical_offset,
+            )
+        elif topo_flag == 6:
+            x_location, y_location, radius = topo_params
+            topo_type = HalfCircleTopo(
+                x_location=int(x_location), y_location=int(y_location), radius=radius
+            )
+        elif topo_flag == 7:
+            period, amplitude = topo_params
+            topo_type = SinusoidTopo(period=period, amplitude=amplitude)
+        elif topo_flag == 8:
+            aspect, height = topo_params
+            topo_type = CosHillTopo(aspect=aspect, height=height)
+        else:
+            topo_type = TopoType(topo_flag=topo_flag)
+        current_line = 3 + add
+        # Smoothing and SOR
+        smoothing_method = int(lines[current_line].strip().split("!")[0])
+        smoothing_passes = int(lines[current_line + 1].strip().split("!")[0])
+        sor_iterations = int(lines[current_line + 2].strip().split("!")[0])
+        sor_cycles = int(lines[current_line + 3].strip().split("!")[0])
+        sor_relax = float(lines[current_line + 4].strip().split("!")[0])
+
+        return cls(
+            filename=filename,
+            topo_type=topo_type,
+            smoothing_method=smoothing_method,
+            smoothing_passes=smoothing_passes,
+            sor_iterations=sor_iterations,
+            sor_cycles=sor_cycles,
+            sor_relax=sor_relax,
+        )
+
+
+class RuntimeAdvancedUserInputs(InputFile):
+    """
+    Class representing the Runtime_Advanced_User_Inputs.inp input file.
+    This file contains advanced parameters related to computer memory usage.
+
+    Attributes
+    ----------
+    num_cpus : PositiveInt
+        Maximum number of CPU to use. Do not exceed 8. Use 1 for ensemble
+        simulations.
+    use_acw : Literal[0,1]
+        Use Adaptive Computation Window (0=Disabled 1=Enabled)
+    """
+
+    name: str = "Runtime_Advanced_User_Inputs"
+    _extension: str = ".inp"
+    num_cpus: PositiveInt = Field(le=8, default=8)
+    use_acw: Literal[0, 1] = 0
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "Runtime_Advanced_User_Inputs.inp", "r") as f:
+            lines = f.readlines()
+
+        return cls(
+            num_cpus=int(lines[0].strip().split("!")[0]),
+            use_acw=int(lines[1].strip().split("!")[0]),
+        )
+
+
+class QU_movingcoords(InputFile):
+    """
+    Class representing the QU_movingcoords.inp input file.
+    This is a QUIC legacy file that is not modified for QUIC-Fire use.
+    """
+
+    name: str = "QU_movingcoords"
+    _extension: str = ".inp"
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        with open(directory / "QU_movingcoords.inp", "r") as f:
+            lines = f.readlines()
+
+        if int(lines[1].strip().split("!")[0]) == 1:
+            print(
+                "WARNING: QU_movingcoords.inp: Moving coordinates flag == 1 not supported."
+            )
+
+        return cls()
+
+
+class QP_buildout(InputFile):
+    """
+    Class representing the QU_buildout.inp input file.
+    This is a QUIC legacy file that is not modified for QUIC-Fire use.
+    """
+
+    name: str = "QP_buildout"
+    _extension: str = ".inp"
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+
+        with open(directory / "QP_buildout.inp", "r") as f:
+            lines = f.readlines()
+
+        if int(lines[0].strip().split("!")[0]) == 1:
+            print("WARNING: QP_buildout.inp: number of buildings will be set to 0.")
+        if int(lines[1].strip().split("!")[0]) == 1:
+            print(
+                "WARNING: QP_buildout.inp: number of vegetative canopies will be set to 0."
+            )
+
+        return cls()
+
+
+class QU_metparams(InputFile):
+    """
+    Class representing the QU_metparams.inp input file.
+    This file contains information about wind profiles
+
+    Attributes
+    ----------
+    num_sensors : int
+        Number of measuring sites. Multiple wind profiles are not yet supported.
+    sensor_name : str
+        Name of the wind profile. This will correspond to the filename of the wind profile, e.g. sensor1.inp
+    """
+
+    name: str = "QU_metparams"
+    _extension: str = ".inp"
+    num_sensors: PositiveInt = 1
+    sensor_name: str = "sensor1"
+
+    @computed_field
+    @property
+    def sensor_lines(self) -> str:
+        return (
+            f"{self.sensor_name} !Site Name\n" f"!File name\n" f"{self.sensor_name}.inp"
+        )
+
+    @classmethod
+    def from_file(cls, directory):
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "QU_metparams.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            num_sensors=int(lines[2].strip().split()[0]),
+            sensor_name=str(lines[4].strip().split()[0].strip()),
+        )
+
+
+class Sensor1(InputFile):
+    """
+    Class representing the sensor1.inp input file.
+    This file contains information on winds, and serves as the
+    primary source for wind speed(s) and direction(s)
+
+    Attributes
+    ----------
+    time_now : PositiveInt
+        Begining of time step in Unix Epoch time (integer seconds since
+        1970/1/1 00:00:00). Must match time at beginning of fire
+        (QU_Simparams.inp and QUIC_fire.inp)
+    sensor_height : PositiveFloat
+        Wind measurement height (m). Default is 6.1m (20ft)
+    wind_speed : PositiveFloat
+        Wind speed (m/s)
+    wind_direction : NonNegativeInt < 360
+        Wind direction (degrees). Use 0° for North
+    """
+
+    name: str = "sensor1"
+    _extension: str = ".inp"
+    time_now: PositiveInt
+    sensor_height: PositiveFloat = 6.1  # 20ft
+    wind_speed: PositiveFloat
+    wind_direction: NonNegativeInt = Field(lt=360)
+
+    @computed_field
+    @property
+    def wind_lines(self) -> str:
+        """
+        This is meant to support wind shifts in the future.
+        This computed field could be altered to reproduce the lines below
+        for a series of times, speeds, and directions.
+        """
+        return (
+            f"{self.time_now} !Begining of time step in Unix Epoch time"
+            f"(integer seconds since 1970/1/1 00:00:00)\n"
+            f"1 !site boundary layer flag (1 = log, 2 = exp, 3 = urban "
+            f"canopy, 4 = discrete data points)\n"
+            f"0.1 !site zo\n"
+            f"0. ! 1/L (default = 0)\n"
+            f"!Height (m),Speed	(m/s), Direction (deg relative to true N)\n"
+            f"{self.sensor_height} {self.wind_speed} {self.wind_direction}"
+        )
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "sensor1.inp", "r") as f:
+            lines = f.readlines()
+        return cls(
+            time_now=int(lines[6].strip().split("!")[0]),
+            sensor_height=float(lines[11].split(" ")[0]),
+            wind_speed=float(lines[11].split(" ")[1]),
+            wind_direction=int(lines[11].split(" ")[2]),
+        )
diff --git a/quicfire_tools/inputs/documentation/IgnitionType.json b/quicfire_tools/inputs/documentation/IgnitionType.json
new file mode 100644
index 0000000..6d29398
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/IgnitionType.json
@@ -0,0 +1,113 @@
+{
+    "rectangle_ignition": {
+        "x_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "x_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    },
+    "square_ring_ignition": {
+        "x_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "x_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "x_width": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Width of the ring in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_width": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Width of the ring in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    },
+    "cicular_ring_ignition": {
+        "x_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_min": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "Any",
+            "description": "South-west corner in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "x_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the x-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_length": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Length in the y-direction",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "ring_width": {
+            "values_accepted": "> 0",
+            "tested_values_range": "Any",
+            "description": "Width of the ring",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QFire_Advanced_User_Inputs.inp.json b/quicfire_tools/inputs/documentation/QFire_Advanced_User_Inputs.inp.json
new file mode 100644
index 0000000..63c5b11
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QFire_Advanced_User_Inputs.inp.json
@@ -0,0 +1,114 @@
+{
+  "fraction_cells_launch_firebrands": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "(0, 1]",
+    "tested_values_range": "0.05-0.9",
+    "description": "Fraction of cells that could launch firebrand tracers from which firebrand tracers will actually be launched. Higher value = more firebrand tracers. Recommended value: 0.05",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "firebrand_radius_scale_factor": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 1",
+    "tested_values_range": "40-80",
+    "description": "Multiplicative factor used to relate the length scale of the mixing (firebrand distribution entrainment length scale) to the initial size of the distribution. Higher value = higher growth rate or RT (firebrand distribution) with flight time. Recommended value: 40",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "firebrand_trajectory_time_step": {
+    "data_type": "PositiveInt",
+    "values_accepted": "\u2265 1",
+    "tested_values_range": "1",
+    "description": "Time step used to determine the firebrand tracer trajectory. Higher value = less accurate trajectory. Recommended value: 1 s",
+    "units": "[s]",
+    "can_be_modified": "Y"
+  },
+  "firebrand_launch_interval": {
+    "data_type": "PositiveInt",
+    "values_accepted": "> 0",
+    "tested_values_range": "10-20",
+    "description": "Time interval between launching of firebrand tracers. Higher value = less firebrand tracers launched. Recommended value: 10 s",
+    "units": "[s]",
+    "can_be_modified": "Y"
+  },
+  "firebrands_per_deposition": {
+    "data_type": "PositiveInt",
+    "values_accepted": "> 0",
+    "tested_values_range": "500",
+    "description": "Number of firebrand tracers that one firebrand tracer launched represents. Recommended value: 500",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "firebrand_area_ratio": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 0",
+    "tested_values_range": "20.",
+    "description": "Multiplicative factor used to relate the cell area and fraction of cells from which tracers are launched to initial area represented by one firebrand.",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "minimum_burn_rate_coefficient": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 0",
+    "tested_values_range": "50.",
+    "description": "Multiplicative factor relating the minimum mass-loss rate that a firebrand tracer needs to have to justify continuing to track its trajectory to the energy associated with a new ignition",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "max_firebrand_thickness_fraction": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.75",
+    "description": "Multiplicative factor relating the thickness of launched firebrand tracer to maximum loftable firebrand thickness",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "firebrand_germination_delay": {
+    "data_type": "PositiveInt",
+    "values_accepted": "> 0",
+    "tested_values_range": "180",
+    "description": "Time after a firebrand has landed at which a fire is started",
+    "units": "[s]",
+    "can_be_modified": "Y"
+  },
+  "vertical_velocity_scale_factor": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 1",
+    "tested_values_range": "5-10",
+    "description": "Maximum value of the multiplicative factor of the vertical velocity experienced by a firebrand = 1/(fraction of the QUIC-URB on fire)",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "minimum_firebrand_ignitions": {
+    "data_type": "PositiveInt",
+    "values_accepted": "> 0",
+    "tested_values_range": "50-100",
+    "description": "Minimum number of ignitions to be sampled in a position where a firebrand lands",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "maximum_firebrand_ignitions": {
+    "data_type": "PositiveInt",
+    "values_accepted": "> 0",
+    "tested_values_range": "100-500",
+    "description": "Maximum number of ignitions sampled at positions distributed within RT around where a firebrand tracer lands",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "minimum_landing_angle": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "[0, PI/2]",
+    "tested_values_range": "PI/3-PI/6",
+    "description": "Minimum value considered for the angle between the trajectory of the firebrand when it hits the ground and horizontal",
+    "units": "[rad]",
+    "can_be_modified": "N"
+  },
+  "maximum_firebrand_thickness": {
+    "data_type": "PositiveFloat",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.03-0.05",
+    "description": "Maximum firebrand's thickness",
+    "units": "[m]",
+    "can_be_modified": "Y"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/QFire_Bldg_Advanced_User_Inputs.inp.json b/quicfire_tools/inputs/documentation/QFire_Bldg_Advanced_User_Inputs.inp.json
new file mode 100644
index 0000000..d34fe4a
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QFire_Bldg_Advanced_User_Inputs.inp.json
@@ -0,0 +1,74 @@
+{
+  "convert_buildings_to_fuel_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Flag to convert QUIC-URB buildings to fuel. 0 = do not convert, 1 = convert.",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "0"
+  },
+  "building_fuel_density": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.5",
+    "description": "Thin fuel density within buildings if no fuel is specified and buildings are converted to fuel. Higher value = more fuel.",
+    "units": "[kg/m^3]",
+    "can_be_modified": "N",
+    "recommended_value": "0.5"
+  },
+  "building_attenuation_coefficient": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-2",
+    "description": "Attenuation coefficient within buildings if buildings are converted to fuel. Higher value = more drag.",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "2"
+  },
+  "building_surface_roughness": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.01-0.1",
+    "description": "Surface roughness within buildings if buildings are converted to fuel. Higher value = lower wind speed.",
+    "units": "[m]",
+    "can_be_modified": "N",
+    "recommended_value": "0.01 m"
+  },
+  "convert_fuel_to_canopy_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Flag to convert fuel to canopy for winds. 0 = do not convert, 1 = convert.",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "1"
+  },
+  "update_canopy_winds_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Flag to update canopy winds when fuel is consumed. 0 = do not update, 1 = update.",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "1"
+  },
+  "fuel_attenuation_coefficient": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-2",
+    "description": "Attenuation coefficient within fuel for the Cionco wind profile. Higher value = more drag.",
+    "units": "[-]",
+    "can_be_modified": "Y",
+    "recommended_value": "1"
+  },
+  "fuel_surface_roughness": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.01-0.1",
+    "description": "Surface roughness within fuel. Higher value = lower wind speed.",
+    "units": "[m]",
+    "can_be_modified": "N",
+    "recommended_value": "0.1 m"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/QFire_Plume_Advanced_User_Inputs.inp.json b/quicfire_tools/inputs/documentation/QFire_Plume_Advanced_User_Inputs.inp.json
new file mode 100644
index 0000000..abc1b39
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QFire_Plume_Advanced_User_Inputs.inp.json
@@ -0,0 +1,132 @@
+{
+  "max_plumes_per_timestep": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "[50k, 500k]",
+    "description": "Maximum number of plumes at each time step. Higher value = slower simulation.",
+    "units": "[-]",
+    "can_be_modified": "Y",
+    "recommended_value": "150,000"
+  },
+  "min_plume_updraft_velocity": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.1-0.5",
+    "description": "Minimum plume updraft velocity. If a plume updraft is below this value, the plume is removed from the simulation. Higher value = less plumes, faster simulation.",
+    "units": "[m/s]",
+    "can_be_modified": "Y",
+    "recommended_value": "0.1 m/s"
+  },
+  "max_plume_updraft_velocity": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "200",
+    "description": "Maximum updraft velocity. Recommended value: 100 m/s.",
+    "units": "[m/s]",
+    "can_be_modified": "Y"
+  },
+  "min_velocity_ratio": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.05-0.1",
+    "description": "Minimum ratio between the plume updraft velocity and the wind speed. If below, the plume is removed from the simulation, unless its updraft is 0.5 m/s or higher. Higher value = less plumes, faster simulation. Recommended value: 0.1.",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "brunt_vaisala_freq_squared": {
+    "data_type": "REAL",
+    "values_accepted": ">= 0",
+    "tested_values_range": "0",
+    "description": "Inverse of the Brunt-Vaisala frequency squared (measure of atmospheric stability). Recommended value: 0 1/s2.",
+    "units": "[1/s2]",
+    "can_be_modified": "Y"
+  },
+  "creeping_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Flag: 0 = do not spread fire by creeping, 1 = spread fire by creeping. Recommended value: 1.",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "adaptive_timestep_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Time step flag: 0 = constant time step, 1 = adaptive time step. Adaptive time step = more accurate plume trajectory, longer simulation time. Recommended value: 0.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "plume_timestep": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-10",
+    "description": "Time step used to compute the buoyant plume trajectory. Higher value = less accurate plume trajectory. Recommended value: 1 s.",
+    "units": "[s]",
+    "can_be_modified": "Y"
+  },
+  "sor_option_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "SOR solver option: 0 = standard SOR, 1 = memory SOR. Recommended value: 1.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "sor_alpha_plume_center": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-10",
+    "description": "SOR alpha! value (plume centerline). Higher value = w-component in the cells affected by the plumes is modified less by the SOR solver. Recommended value: 10.",
+    "units": "[-]",
+    "can_be_modified": "N"
+  },
+  "sor_alpha_plume_edge": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-10",
+    "description": "SOR alpha! value (plume periphery). Higher value = w-component in the cells affected by the plumes is modified less by the SOR solver. Recommended value: 1.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "max_plume_merging_angle": {
+    "data_type": "REAL",
+    "values_accepted": "(0, 180]",
+    "tested_values_range": "30-60",
+    "description": "Maximum angle between plumes to determine if they can be merged. Higher value = more plumes merged. Recommended value: 30 deg.",
+    "units": "[deg]",
+    "can_be_modified": "N"
+  },
+  "max_plume_overlap_fraction": {
+    "data_type": "REAL",
+    "values_accepted": "(0, 1]",
+    "tested_values_range": "0.7-1",
+    "description": "Maximum fraction of a plume P trajectory length used to determine if a plume Q overlaps enough to be merged. Higher value = more plumes merged. Recommended value: 0.7.",
+    "units": "[deg]",
+    "can_be_modified": "N"
+  },
+  "plume_to_grid_updrafts_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Method to compute the plume-to-grid updrafts. 0 = new method, needed if smoke is simulated afterwards. Takes longer. 1 = old method. Recommended value: 1.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "max_points_along_plume_edge": {
+    "data_type": "INT",
+    "values_accepted": "[1, 100]",
+    "tested_values_range": "[2-10]",
+    "description": "How many points to sample along the QU cells edges for the new method (line above). Recommended value: 10.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "plume_to_grid_intersection_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Scheme to compute the plume-to-grid updrafts when more than one plume intersects with a QU cell. 0 = use the cube method. 1 = use the max value. Recommended value: 1.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/QP_buildout.inp.json b/quicfire_tools/inputs/documentation/QP_buildout.inp.json
new file mode 100644
index 0000000..97908d8
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QP_buildout.inp.json
@@ -0,0 +1,12 @@
+{
+    "num_buildings":{
+        "values_accepted": "0",
+        "description": "total number of buildings",
+        "can_be_modified": "N"
+    },
+    "num_canopies": {
+        "values_accepted": "0",
+        "description": "total number of vegitative canopies",
+        "can_be_modified": "N"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QUIC_fire.inp.json b/quicfire_tools/inputs/documentation/QUIC_fire.inp.json
new file mode 100644
index 0000000..714cf9f
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QUIC_fire.inp.json
@@ -0,0 +1,277 @@
+{
+    "fire_flag": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "0 = only QUIC-URB simulation, no other input required",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "random_seed": {
+        "values_accepted": "[-1 +Inf)",
+        "tested_values_range": "Any",
+        "description": "accepted: -1 = use time and date to define the random number seed, >0 = use specified value to initialize random number generator",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "Any integer > 0"
+    },
+    "time_now": {
+        "values_accepted": ">= 0",
+        "tested_values_range": "Any",
+        "description": "When the fire is ignited in Unix Epoch time, i.e., integer seconds since 1/1/1970 00:00:00 (fire must start at or after the first wind, see QU_simparams.inp)",
+        "units": "[s]",
+        "can_be_modified": "N"
+    },
+    "sim_time": {
+        "values_accepted": "> 0",
+        "tested_values_range": "(0 10800]",
+        "description": "Total simulation time for the fire model. Higher value = longer simulation",
+        "units": "[s]",
+        "can_be_modified": "Y"
+    },
+    "fire_time_step": {
+        "values_accepted": "> 0",
+        "tested_values_range": "1-10",
+        "description": "Time step for the simulation for the fire model. Higher value = less accurate simulation",
+        "units": "[s]",
+        "can_be_modified": "N",
+        "recommended_value": "1 s"
+    },
+    "quic_time_step": {
+        "values_accepted": ">= 1",
+        "tested_values_range": "1-5",
+        "description": "Number of fire time steps done before updating the wind field. Higher value = less accurate simulation (less coupling between winds and fire)",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "1 to 5"
+    },
+    "out_time_fire": {
+        "values_accepted": ">= 1 or -1",
+        "tested_values_range": "1-100 and -1",
+        "description": "Number of updates of the fire before printing the output files for the fire model (all but emissions and thermal radiation). Higher value = less output. Use -1 to provide custom times in file QFire_ListOutputTimes.inp",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "4 or higher."
+    },
+    "out_time_wind": {
+        "values_accepted": ">= 1 or -1",
+        "tested_values_range": "1-100 and -1",
+        "description": "Number of updates of the wind field before printing the output files for the instantaneous QUIC-URB winds. Higher value = less output. Recommended value 2 or higher. Use -1 to provide custom times in file QFire_ListOutputTimes.inp",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "2 or higher."
+    },
+    "out_time_emis_rad": {
+        "values_accepted": ">= 1 or -1",
+        "tested_values_range": "1-100 and -1",
+        "description": "Number of updates of the fire before printing the output files for the emissions and thermal radiation. Higher value = less output. Use -1 to provide custom times in file QFire_ListOutputTimes.inp",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "out_time_wind_avg": {
+        "values_accepted": ">= 1 or -1",
+        "tested_values_range": "1-100 and -1",
+        "description": "Number of updates of the wind field before printing the output files for the average QUIC- URB winds (needed for smoke calculations). Higher value = less output. Use -1 to provide custom times in file QFire_ListOutputTimes.inp",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "nz": {
+        "values_accepted": ">= 1",
+        "tested_values_range": "1-100",
+        "description": "Number of cells in the vertical direction for the fire model. See below how to determine the number of cells.",
+        "units": "[-]",
+        "can_be_modified": "N"
+    },
+    "stretch_grid_flag": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0",
+        "description": "Vertical spacing model: 0 = constant dz, only one value must follow",
+        "units": "[-]",
+        "can_be_modified": "N",
+        "recommended_value": "0"
+    },
+    "dz_array": {
+        "values_accepted": "> 0",
+        "tested_values_range": "1-10",
+        "description": "Fire model cell size in the vertical direction. If stretchflag == 0: defaults to 1 m. If stretchflag == 1: list of values. Number of values must equal number of cells in the vertical direction. Higher value = less accurate fire and fuel description",
+        "units": "[m]",
+        "can_be_modified": "Y",
+        "recommended_value": "1 m"
+    },
+    "file_path": {
+        "values_accepted": "-",
+        "tested_values_range": "Any",
+        "description": "Files it refers to: treesrhof.dat; treesmoist.dat; treesfueldepth.dat; ignite.dat; rasterorigin.txt; gridlist. Empty quotes '': Firetec files are in the same folders of all other inputs. Path within quotes, with folder separator at the end: Firetec files are in teh specified folder",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "fuel_file_type": {
+        "values_accepted": "[1-2]",
+        "tested_values_range": "1-2",
+        "description": "Format of the trees* files: 1 = all fuel types are in one file; 2 = files are broken down by fuel type and named with '_#' to reduce peak RAM use",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "fuel_file_format": {
+        "values_accepted": "[1-2]",
+        "tested_values_range": "1-2",
+        "description": "Format of the trees* files: 1 = binary files have stream format (ARA format); 2 = binary files have Fortran headers",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "1"
+    },
+    "fuel_flag": {
+        "values_accepted": "[1-4]",
+        "tested_values_range": "1-4",
+        "description": "Flag for fuel density, moisture, and height: 1 = density/moisture/height is uniform over the domain and provided with the next line input; 2 = density/moisture/height is provided through QF_FuelDensity.inp/QF_FuelMoisture.inp/QF_FuelHeight.inp; 3 = density/moisture/height is provided through Firetec file (treesrhof.dat/treesmoist.dat/treesdepth.dat) matching QUIC-Fire grid; 4 = density/moisture/height is provided through Firetec files for an arbitrary grid. The files gridlist and rasterorigin.txt must also be provided",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "fuel_density": {
+        "values_accepted": "> 0",
+        "tested_values_range": "0.2-3",
+        "description": "Fuel density",
+        "units": "[kg/m**3]",
+        "can_be_modified": "Y"
+    },
+    "fuel_moisture": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0-1",
+        "description": "Fuel moisture = mass of water/mass of dry fuel",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "fuel_height": {
+        "values_accepted": "> 0",
+        "tested_values_range": "0.7-3.0",
+        "description": "Fuel height",
+        "units": "[m]",
+        "can_be_modified": "Y"
+    },
+    "ignition_flag": {
+        "values_accepted": "[1 6]",
+        "tested_values_range": "1-6",
+        "description": "Ignition source shape flag: 1 = rectangle; 2 = square ring; 3 = circular ring; 4 = initial ignition locations provided through file QF_Ignitions.inp; 5 = time- and space-dependent ignition locations provided through file QF_IgnitionPattern.inp; 6 = ignite.dat (Firetec file); (NOTE: Flag of 7 seems to work better for ignite.dat - NJT)",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "6 (NJT recommendation: 7)"
+    },
+    "ignitions_per_cell": {
+        "values_accepted": "> 0",
+        "tested_values_range": "10-10000",
+        "description": "Number of ignition per cell of the fire model",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "100 (seems to have no effect). (CHANGED RECOMMENDATION PER QF MEETINGS: 2)"
+    },
+    "firebrand_flag": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag: 0 = firrebrands are off; 1 = firebrands are on",
+        "units": "[-]",
+        "can_be_modified": "N",
+        "recommended_value": "0 (firebrands are untested for small scale problems)"
+    },
+    "eng_to_atm_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded energy-to-atmoshpere. File name: fire-energy_to_atmos-XXXXX.bin",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "react_rate_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded reaction rate. File name: fire-reaction_rate-XXXXX.bin",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "fuel_dens_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded fuel density. File name: fuels-dens-XXXXX.bin. NOTE: if 'emissions_out' is set to 4, this flag is set to 1 independently from the user choice",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "QF_wind_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded wind components (u,v,w) for the fire grid. File names: winduXXXXX.bin = x-component; windvXXXXX.bin = y-component; windwXXXXX.bin = z-component",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "QU_wind_inst_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded wind components (u,v,w) for the QUIC-URB grid. File names: qf_winduXXXXX.bin = x-component; qf_windvXXXXX.bin = y-component; qu_windwXXXXX.bin = z-component; qu_wplumeXXXXX.bin = w field from buoyant plumes, before mass consistency",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "QU_wind_avg_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the gridded average wind components (u,v,w) for the QUIC-URB grid. File names: qf_windu_aveXXXXX.bin = x-component; qu_windv_aveXXXXX.bin = y-component; qu_windw_aveXXXXX.bin = z-component. Set to 1 only if QUIC-Smoke is run for PM2.5 and CO plume transport and dispersion simulations",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "0"
+    },
+    "plume_traj_out": {
+        "values_accepted": "[0 2]",
+        "tested_values_range": "0, 2",
+        "description": "Flag to output the plume trajectories: 0 = do not output; 1 = use csv format; 2 = use binary format. File names: plume_trajectory.bin or csv; plume_mergetrajectory.bin or csv",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "0 (Files will be huge, used for debugging only)"
+    },
+    "fuel_moist_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the fuel moisture. File name: fuels-moist-XXXXX.bin",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "mass_burnt_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the vertically-integrated % mass burnt. File name: mburnt_integ-XXXXX.bin",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "firebrand_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the firebrands trajectories. File name: firebrabrands_traj.bin",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "0"
+    },
+    "emissions_out": {
+        "values_accepted": "[0 4]",
+        "tested_values_range": "[0 4]",
+        "description": "Flag for emissions: 0 = do not output any emission related variables; 1 = output emissions fiiles and simulate CO in QUIC-SMOKE; 2 = output emissions files and simulate PM2.5 in QUIC-SMOKE; 3 = output emissions files and simulate both CO and PM2.5 in QUIC-SMOKE; 4 = output emissions files but use library approach in QUIC-SMOKE; 5 = output emissions files and simulate both water in QUIC-SMOKE. File names: pm_emissions-XXXXX.bin; emissions_distribution-XXXXX.bin; co_emissions-XXXXX.bin; water_emissions-XXXXX.bin. Note; to run smoke transport and dispersion, the 'QU_winds_avg_out' must be set to 1 too",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "radiation_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) the thermal radiation. File names: thermalradiation-XXXXX.bin; thermaldose-XXXXX.bin",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "0"
+    },
+    "intensity_out": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to output (1) or not (0) surface fire intensity at every fire time step",
+        "units": "[-]",
+        "can_be_modified": "Y",
+        "recommended_value": "0"
+    },
+    "auto_kill": {
+        "values_accepted": "[0 1]",
+        "tested_values_range": "0, 1",
+        "description": "Flag to kill (1) or not (0) the simulation oif the fire goes out and there are no more ignitions or firebrands or plumes. The fuel and winds will be dumped at this last time step",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_Simparams.inp.json b/quicfire_tools/inputs/documentation/QU_Simparams.inp.json
new file mode 100644
index 0000000..72f0ed0
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_Simparams.inp.json
@@ -0,0 +1,175 @@
+{
+  "nx": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "100-2000",
+    "description": "Number of cells in the x-direction",
+    "units": "[-]",
+    "recommended_value": "> 100"
+  },
+  "ny": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "100-2000",
+    "description": "Number of cells in the y-direction",
+    "units": "[-]",
+    "recommended_value": "> 100"
+  },
+  "nz": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "10-100",
+    "description": "Number of cells in the z-direction",
+    "units": "[-]"
+  },
+  "dx": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.5-250",
+    "description": "Cell size in the x-direction",
+    "units": "[m]",
+    "recommended_value": "2 m"
+  },
+  "dy": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.5-250",
+    "description": "Cell size in the y-direction",
+    "units": "[m]",
+    "recommended_value": "2 m"
+  },
+  "stretch_grid_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1, 2, 3, 4]",
+    "tested_values_range": "[0, 1, 3]",
+    "description": "Vertical grid stretching flag",
+    "units": "[-]",
+    "recommended_value": "3"
+  },
+  "surface_vertical_cell_size": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.5-5",
+    "description": "Surface vertical cell size",
+    "units": "[m]"
+  },
+  "number_surface_cells": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "1-5",
+    "description": "Number of cells with uniform cell size at the surface",
+    "units": "[-]"
+  },
+  "dz_array": {
+    "data_type": "REAL",
+    "values_accepted": "> 0",
+    "tested_values_range": "0.5-200",
+    "description": "Values of the vertical cell sizes",
+    "units": "[m]"
+  },
+  "utc_offset": {
+    "data_type": "INT",
+    "values_accepted": "[-12, +12]",
+    "tested_values_range": "-12 - +12",
+    "description": "Hours difference from UTM",
+    "units": "[h]"
+  },
+  "wind_times": {
+    "data_type": "INT",
+    "values_accepted": "0 - Inf",
+    "tested_values_range": "0 - Inf",
+    "description": "List of times at which the winds are available in Unix Epoch time",
+    "units": "[s]"
+  },
+  "sor_iter_max": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "[1, 500]",
+    "description": "Maximum number of iterations of the SOR wind solver",
+    "units": "[-]",
+    "recommended_value": "10"
+  },
+  "sor_residual_reduction": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "[1, 5]",
+    "description": "Residual reduction to assess convergence of the SOR solver",
+    "units": "[-]",
+    "recommended_value": "3"
+  },
+  "use_diffusion_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Use diffusion algorithm",
+    "units": "[-]",
+    "recommended_value": "0"
+  },
+  "number_diffusion_iterations": {
+    "data_type": "INT",
+    "values_accepted": "> 0",
+    "tested_values_range": "[0, 50]",
+    "description": "Number of diffusion iterations",
+    "units": "[-]"
+  },
+  "domain_rotation": {
+    "data_type": "REAL",
+    "values_accepted": "[-90, +90]",
+    "tested_values_range": "[-90, +90]",
+    "description": "Domain rotation relative to true north",
+    "units": "[degrees]",
+    "recommended_value": "0 deg"
+  },
+  "utm_x": {
+    "data_type": "REAL",
+    "values_accepted": "Can go across UTM zones",
+    "tested_values_range": "",
+    "description": "UTM-x coordinates of the south-west corner of domain",
+    "units": "[m]"
+  },
+  "utm_y": {
+    "data_type": "REAL",
+    "values_accepted": "Can go across UTM zones",
+    "tested_values_range": "",
+    "description": "UTM-y coordinates of the south-west corner of domain",
+    "units": "[m]"
+  },
+  "utm_zone_number": {
+    "data_type": "INT",
+    "values_accepted": "[1, 60]",
+    "tested_values_range": "[1, 60]",
+    "description": "UTM zone number",
+    "units": "[-]"
+  },
+  "utm_zone_letter": {
+    "data_type": "INT",
+    "values_accepted": "[1, 26]",
+    "tested_values_range": "[1, 26]",
+    "description": "UTM zone letter (A=1, B=2,..)",
+    "units": "[-]"
+  },
+  "quic_cfd_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "QUIC-CFD flag",
+    "units": "[-]",
+    "recommended_value": "0"
+  },
+  "explosive_bldg_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Explosive building damage flag",
+    "units": "[-]",
+    "recommended_value": "0"
+  },
+  "bldg_array_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0, 1]",
+    "tested_values_range": "[0, 1]",
+    "description": "Building array flag",
+    "units": "[-]",
+    "recommended_value": "0"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_TopoInputs.inp.json b/quicfire_tools/inputs/documentation/QU_TopoInputs.inp.json
new file mode 100644
index 0000000..30ab09a
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_TopoInputs.inp.json
@@ -0,0 +1,55 @@
+{
+    "filename": {
+        "values_accepted": "Any",
+        "tested_values_range": "Any",
+        "description": "Path to the custom topo file (only used with option 5). Cannot be .bin. Use .dat or .inp",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "topo_flag": {
+        "values_accepted": "[0 2]",
+        "tested_values_range": "[0 2]",
+        "description": "Topo flag: 0 = no terrain file provided, QUIC-Fire is run with flat terrain, 1 = Gaussian hill, 2 = hill pass, 3 = slope mesa, 4 = canyon, 5 = custom, 6 = half circle, 7 = sinusoid, 8 = cos hill, 9 = terrain is provided via QP_elevation.bin (see Section 2.7), 10 = terrain is provided via terrainOutput.txt, 11 = terrain.dat (firetec)",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "smoothing_method": {
+        "values_accepted": "[0 2]",
+        "tested_values_range": "[0 2]",
+        "description": "Smoothing method: 0 = none (default for idealized topo), 1 = Blur, 2 = David’s method based on second derivative",
+        "units": "[-]",
+        "recommended_value": "2 for real topo",
+        "can_be_modified": "Y"
+    },
+    "smoothing_passes": {
+        "values_accepted": "[0 500]",
+        "tested_values_range": "[0 200]",
+        "description": "Number of smoothing passes. Real terrain MUST be smoothed.",
+        "units": "[-]",
+        "recommended_value": "200 for real topo"
+    },
+    "sor_iterations": {
+        "values_accepted": "[0 500]",
+        "tested_values_range": "[0 500]",
+        "description": "Number of SOR iteration to define background winds before starting the fire",
+        "units": "[-]",
+        "recommended_value": "500 for real topo",
+        "can_be_modified": "Y"
+    },
+    "sor_cycles": {
+        "values_accepted": "[0 10]",
+        "tested_values_range": "[0 4]",
+        "description": "Number of times the SOR solver initial fields is reset to define background winds before starting the fire",
+        "units": "[-]",
+        "recommended_value": "4 for real topo",
+        "can_be_modified": "Y"
+    },
+    "sor_relax": {
+        "values_accepted": "[0 2]",
+        "tested_values_range": "[0.9 1.8]",
+        "description": "SOR overrelaxation coefficient. Only used if there is topo.",
+        "units": "[-]",
+        "recommended_value": "0.9-1.3 for real topo",
+        "can_be_modified": "Y"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_buildings.inp.json b/quicfire_tools/inputs/documentation/QU_buildings.inp.json
new file mode 100644
index 0000000..13e959b
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_buildings.inp.json
@@ -0,0 +1,25 @@
+{
+  "wall_roughness_length": {
+        "values_accepted": "> 0",
+        "tested_values_range": "> 0",
+        "description": "Wall roughness length",
+        "units": "[m]",
+        "can_be_modified": "N"
+    },
+    "number_of_buildings": {
+        "values_accepted": "> 0",
+        "tested_values_range": "[1, 30000]",
+        "description": "Number of buildings",
+        "units": "[-]",
+        "can_be_modified": "N",
+        "recommended_value": "0 (building algorithms are not part of QUIC-Fire)"
+    },
+    "number_of_polygon_nodes": {
+        "values_accepted": "> 0",
+        "tested_values_range": "[1, 500]",
+        "description": "Number of polygon building nodes",
+        "units": "[-]",
+        "can_be_modified": "N",
+        "recommended_value": "0"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_fileoptions.inp.json b/quicfire_tools/inputs/documentation/QU_fileoptions.inp.json
new file mode 100644
index 0000000..18961d9
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_fileoptions.inp.json
@@ -0,0 +1,51 @@
+{
+  "output_data_file_format_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0 3]",
+    "tested_values_range": "[0 3]",
+    "description": "Output data file format flag: 1 = ascii, 2 = binary, 3 = both",
+    "values": {
+      "1": "ascii",
+      "2": "binary",
+      "3": "both"
+    },
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "2"
+  },
+  "non_mass_conserved_initial_field_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0 1]",
+    "tested_values_range": "[0 1]",
+    "description": "Flag to write out non-mass conserved initial field (uofield.dat): 0 = do not write, 1 = write",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "0"
+  },
+  "initial_sensor_velocity_field_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0 1]",
+    "tested_values_range": "[0 1]",
+    "description": "Flag to write out the file uosensorfield.dat, the initial sensor velocity field: 0 = do not write, 1 = write",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "0"
+  },
+  "qu_staggered_velocity_file_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0 1]",
+    "tested_values_range": "[0 1]",
+    "description": "Flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure: 0 = do not write, 1 = write",
+    "units": "[-]",
+    "can_be_modified": "N",
+    "recommended_value": "0"
+  },
+  "generate_wind_startup_files_flag": {
+    "data_type": "INT",
+    "values_accepted": "[0 1]",
+    "tested_values_range": "[0 1]",
+    "description": "Generate wind startup files for ensemble simulations: 0 = do not write, 1 = write",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_metparams.inp.json b/quicfire_tools/inputs/documentation/QU_metparams.inp.json
new file mode 100644
index 0000000..33280a5
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_metparams.inp.json
@@ -0,0 +1,16 @@
+{
+    "num_sensors": {
+        "values_accepted": "1",
+        "tested_values_range": "[1 Inf)",
+        "description": "Number of measuring sites",
+        "units": "[-]",
+        "can_be_modified": "N"
+    },
+    "sensor_name": {
+        "values_acepted": "sensor1",
+        "tested_values_range": "N/A",
+        "description": "Name of the wind profile",
+        "units": "[-]",
+        "can_be_modified": "N"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/QU_movingcoords.inp.json b/quicfire_tools/inputs/documentation/QU_movingcoords.inp.json
new file mode 100644
index 0000000..432be37
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/QU_movingcoords.inp.json
@@ -0,0 +1,7 @@
+{
+    "moving_coordinates_flag": {
+        "values_accepted": "0",
+        "description": "Moving coordinates flag (0=no, 1=yes)",
+        "can_be_modified": "N"
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/Runtime_Advanced_User_Inputs.inp.json b/quicfire_tools/inputs/documentation/Runtime_Advanced_User_Inputs.inp.json
new file mode 100644
index 0000000..5459233
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/Runtime_Advanced_User_Inputs.inp.json
@@ -0,0 +1,17 @@
+{
+  "num_cpus":{
+    "data_type": "PositiveInt",
+    "values_accepted": ">0",
+    "tested_values_range": "[1, 8]",
+    "description": "Maximum number of CPU to use. Do not exceed 8. Use 1 for ensemble simulations.",
+    "units": "[-]",
+    "can_be_modified": "Y"
+  },
+  "use_acw":{
+    "data_type": "Literal[0, 1]",
+    "values_accepted": "0,1",
+    "tested_values_range": "[0,1]",
+    "description": "Use Adaptive Computation Window (0=Disabled 1=Enabled)",
+    "can_be_modified": "Y"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/TopoType.json b/quicfire_tools/inputs/documentation/TopoType.json
new file mode 100644
index 0000000..cdf912f
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/TopoType.json
@@ -0,0 +1,163 @@
+{
+    "gaussian_hill_topo": {
+        "x_hilltop": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "[0 Lx]",
+            "description": "Gaussian hill top location x-dir",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_hilltop": {
+            "values_accepted": "[0 Ly]",
+            "tested_values_range": "[0 Ly]",
+            "description": "Gaussian hill top location y-dir",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "elevation_max" : {
+            "values_accepted": "[0 Lz/3]",
+            "tested_values_range": "[0 Lz/3]",
+            "description": "Gaussian hill max elevation",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "elevation_std": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Gaussian hill standard deviation",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    },
+    "hill_pass_topo": {
+        "max_height": {
+            "values_accepted": "[0 Lz/3]",
+            "tested_values_range": "[0 Lz/3]",
+            "description": "Hill pass maximum height",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "location_param": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Hill pass location parameter",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    },
+    "slope_mesa_topo": {
+        "slope_axis": {
+            "values_accepted": "[0 1]",
+            "tested_values_range": "[0 1]",
+            "description": "Slope axis: 0=X, 1=Y",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "slope_value": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Slope in dh/dx or dh/dy",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        },
+        "flat_fraction": {
+            "values_accepted": "[0 1]",
+            "tested_values_range": "[0 1]",
+            "description": "Fraction of domain that is flat",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        }
+    },
+    "canyon_topo": {
+        "x_start": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "[0 Lx]",
+            "description": "Canyon location in x-dir",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_center": {
+            "values_accepted": "[0 Ly]",
+            "tested_values_range": "[0 Ly]",
+            "description": "Canyon location in x-dir",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "slope_value": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Slope in dh/dx or dh/dy",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        },
+        "canyon_std": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Canyon function standard deviation",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "vertical_offset": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Canyon vertical offset",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        }
+    },
+    "half_circle_topo": {
+        "x_location": {
+            "values_accepted": "[0 Lx]",
+            "tested_values_range": "[0 Lx]",
+            "description": "Half circle x-location",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "y_location": {
+            "values_accepted": "[0 Ly]",
+            "tested_values_range": "[0 Ly]",
+            "description": "Half circle y-location",
+            "units": "[m]",
+            "can_be_modified": "Y"
+        },
+        "radius": {
+            "values_accepted": ">0",
+            "tested_values_range": "(0 Lx/2)",
+            "description": "Half circle radius",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        }
+    },
+    "sinusoid_topo": {
+        "period": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Sinusoid period",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        },
+        "amplitude": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Sinusoid amplitude",
+            "units": "[-]",
+            "can_be_modified": "Y"
+        }
+    },
+    "cos_hill_topo": {
+        "aspect": {
+            "values_accepted": "> 0",
+            "tested_values_range": "> 0",
+            "description": "Hill aspect",
+            "units": "[-] (likely degree)",
+            "can_be_modified": "Y"
+        },
+        "height": {
+            "values_accepted": "[0 Lz/3]",
+            "tested_values_range": "[0 Lz/3]",
+            "description": "Hill height",
+            "units": "[-] (likely m)",
+            "can_be_modified": "Y"
+        }
+    }
+}
diff --git a/quicfire_tools/inputs/documentation/gridlist.json b/quicfire_tools/inputs/documentation/gridlist.json
new file mode 100644
index 0000000..68a9c78
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/gridlist.json
@@ -0,0 +1,46 @@
+{
+  "n": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "description": "Number of cells in the x-direction",
+      "units": "[-]"
+  },
+  "m": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "description": "Number of cells in the y-direction",
+      "units": "[-]"
+  },
+  "l": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "description": "Number of cells in the z-direction",
+      "units": "[-]"
+  },
+  "dx": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "recommended_value": "2m",
+      "description": "Cell size in the x-direction",
+      "units": "[m]"
+  },
+  "dy": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "recommended_value": "2m",
+      "description": "Cell size in the y-direction",
+      "units": "[m]"
+  },
+  "dz": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "description": "Number that multiplied by the number of cells  in the z-direction would provide the height of the domain",
+      "units": "[m]"
+  },
+  "aa1": {
+      "values_accepted": "> 0",
+      "tested_values_range": "> 0",
+      "description": "Stretching factor for the vertical grid spacing",
+      "units": "[-]"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/rasterorigin.txt.json b/quicfire_tools/inputs/documentation/rasterorigin.txt.json
new file mode 100644
index 0000000..6e441d3
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/rasterorigin.txt.json
@@ -0,0 +1,17 @@
+{
+  "utm_x": {
+    "values_accepted": "> 0",
+    "tested_values_range": "> 0",
+    "description": "South-west corner of the Firetec domain, UTM-x",
+    "units": "[m]",
+    "variable_name_fortran": "ft%utmx"
+  },
+
+  "utm_y": {
+    "values_accepted": "> 0",
+    "tested_values_range": "> 0",
+    "description": "South-west corner of the Firetec domain, UTM-y",
+    "units": "[m]",
+    "variable_name_fortran": "ft%utmy"
+  }
+}
diff --git a/quicfire_tools/inputs/documentation/sensor1.inp.json b/quicfire_tools/inputs/documentation/sensor1.inp.json
new file mode 100644
index 0000000..e07423a
--- /dev/null
+++ b/quicfire_tools/inputs/documentation/sensor1.inp.json
@@ -0,0 +1,34 @@
+{
+    "time_now":{
+        "data_type": "NonNegativeInt",
+        "values_accepted": "[0 Inf)",
+        "tested_values": "[0 Inf)",
+        "description": "Beginning of time step in Unix Epoch time (integer seconds since 1970/1/1 00:00:00)",
+        "units": "[-]",
+        "can_be_modified": "Y"
+    },
+    "sensor_height": {
+        "data_type": "PositiveFloat",
+        "values_accepted": ">0",
+        "tested_values": "N/A",
+        "description": "Wind measurement height",
+        "units": "[m]",
+        "can_be_modified": "Y"
+    },
+    "wind_speed": {
+        "data_type": "PositiveFloat",
+        "values_accepted": ">0",
+        "tested_values": "N/A",
+        "description": "Wind speed",
+        "units": "[m/s]",
+        "can_be_modified": "Y"
+    },
+    "wind_direction": {
+        "data_type": "PositiveInt",
+        "values_accepted": "[0 359]",
+        "tested_values": "[0 359]",
+        "description": "Wind direction",
+        "units": "[degree]",
+        "can_be_modified": "Y"
+    }
+}
diff --git a/quicfire_tools/input-templates/latest/QFire_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.1.1/QFire_Advanced_User_Inputs.inp
old mode 100755
new mode 100644
similarity index 98%
rename from quicfire_tools/input-templates/latest/QFire_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QFire_Advanced_User_Inputs.inp
index 5ff63c7..6c70b36
--- a/quicfire_tools/input-templates/latest/QFire_Advanced_User_Inputs.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/QFire_Advanced_User_Inputs.inp
@@ -1,14 +1,14 @@
-0.05			! N/A, fraction of the cells on fire that will launch a firebrand
-40.			! N/A, scaling factor of the radius represented by the firebrands launched
-1				! s, time step for the firebrands trajectory calculation
-10				! s, how often to launch firebrands
-500			! N/A, number of firebrands distributed over the landing area
-20.			! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
-50.			! N/A, min_b_value_coef
-0.75			! N/A, fb_frac_of_max_size
-180			! s, germination_delay
-5.				! N/A, fraction of the cell on fire (to scale w)
-50				! N/A, minimum number of ignitions via firebrands at a point
-100			! N/A, maximum number of ignitions via firebrands at a point
-0.523598		! rad, min_theta_value (pi/6)
-0.03        ! m, fb thickness
+0.05			! N/A, fraction of the cells on fire that will launch a firebrand
+40.			! N/A, scaling factor of the radius represented by the firebrands launched
+1				! s, time step for the firebrands trajectory calculation
+10				! s, how often to launch firebrands
+500			! N/A, number of firebrands distributed over the landing area
+20.			! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
+50.			! N/A, min_b_value_coef
+0.75			! N/A, fb_frac_of_max_size
+180			! s, germination_delay
+5.				! N/A, fraction of the cell on fire (to scale w)
+50				! N/A, minimum number of ignitions via firebrands at a point
+100			! N/A, maximum number of ignitions via firebrands at a point
+0.523598		! rad, min_theta_value (pi/6)
+0.03        ! m, fb thickness
diff --git a/quicfire_tools/input-templates/v5.1.1/QFire_Bldg_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.1.1/QFire_Bldg_Advanced_User_Inputs.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QFire_Bldg_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QFire_Bldg_Advanced_User_Inputs.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp
similarity index 93%
rename from quicfire_tools/input-templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp
index 67b317e..b0b462e 100644
--- a/quicfire_tools/input-templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/QFire_Plume_Advanced_User_Inputs.inp
@@ -10,7 +10,7 @@
 10             ! alpha 2 - plume centerline
 2              ! alpha 2 - plume edges
 30.            ! deg, max angle between plumes to merging
-0.7            ! N/A, fraction of overlap of plumes for merging 
+0.7            ! N/A, fraction of overlap of plumes for merging
 1              ! N/A, which wplume-to-grid scheme: 0 = new, 1 = old
 10             ! N/A, number of points per edge for new scheme (only new scheme)
 1              ! N/A, w computation: 1 = max(w), 0 = sum(w^3)
diff --git a/quicfire_tools/input-templates/v5.1.1/QP_buildout.inp b/quicfire_tools/inputs/templates/v5.1.1/QP_buildout.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QP_buildout.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QP_buildout.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QUIC_fire.inp b/quicfire_tools/inputs/templates/v5.1.1/QUIC_fire.inp
similarity index 98%
rename from quicfire_tools/input-templates/v5.1.1/QUIC_fire.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QUIC_fire.inp
index 397a7a5..c8d6e63 100644
--- a/quicfire_tools/input-templates/v5.1.1/QUIC_fire.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/QUIC_fire.inp
@@ -40,4 +40,4 @@ $ignition_flag      ! 1 = rectangle, 2 = square ring, 3 = circular ring, 4 = fil
 0					! Output gridded thermal radiation (fire grid)
 0                   ! Output surface fire intensity at every fire time step
 ! AUTOKILL
-$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
\ No newline at end of file
+$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_TopoInputs.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_TopoInputs.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QU_TopoInputs.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_TopoInputs.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_buildings.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_buildings.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QU_buildings.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_buildings.inp
diff --git a/quicfire_tools/input-templates/latest/QU_fileoptions.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_fileoptions.inp
old mode 100755
new mode 100644
similarity index 86%
rename from quicfire_tools/input-templates/latest/QU_fileoptions.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_fileoptions.inp
index cd9678d..605458f
--- a/quicfire_tools/input-templates/latest/QU_fileoptions.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/QU_fileoptions.inp
@@ -1,6 +1,6 @@
-! QUIC 6.26 File Header
-2   !output data file format flag (1=ascii, 2=binary, 3=both)
-0   !flag to write out non-mass conserved initial field (uofield.dat) (1=write,0=no write)
-0   !flag to write out the file uosensorfield.dat, the initial sensor velocity field (1=write,0=no write)
-0   !flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure(1=write,0=no write)
-0   !flag to generate startup files (0 = no, 1 = yes)
\ No newline at end of file
+! QUIC 6.26 File Header
+2   !output data file format flag (1=ascii, 2=binary, 3=both)
+0   !flag to write out non-mass conserved initial field (uofield.dat) (1=write,0=no write)
+0   !flag to write out the file uosensorfield.dat, the initial sensor velocity field (1=write,0=no write)
+0   !flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure(1=write,0=no write)
+0   !flag to generate startup files (0 = no, 1 = yes)
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_landuse.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_landuse.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QU_landuse.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_landuse.inp
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_metparams.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_metparams.inp
similarity index 88%
rename from quicfire_tools/input-templates/v5.2.3/QU_metparams.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_metparams.inp
index a41fb25..988c4ef 100644
--- a/quicfire_tools/input-templates/v5.2.3/QU_metparams.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/QU_metparams.inp
@@ -2,6 +2,6 @@
 0 !Met input flag (0=QUIC,1=WRF,2=ITT MM5,3=HOTMAC)
 1 !Number of measuring sites
 1 !Maximum size of data points profiles
-sensor1 !Site Name 
+sensor1 !Site Name
 !File name
 sensor1.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_movingcoords.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_movingcoords.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QU_movingcoords.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_movingcoords.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_simparams.inp b/quicfire_tools/inputs/templates/v5.1.1/QU_simparams.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.1.1/QU_simparams.inp
rename to quicfire_tools/inputs/templates/v5.1.1/QU_simparams.inp
diff --git a/quicfire_tools/inputs/templates/v5.1.1/Runtime_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.1.1/Runtime_Advanced_User_Inputs.inp
new file mode 100644
index 0000000..980f37f
--- /dev/null
+++ b/quicfire_tools/inputs/templates/v5.1.1/Runtime_Advanced_User_Inputs.inp
@@ -0,0 +1,1 @@
+$num_cpus  ! max number of cpu
diff --git a/quicfire_tools/input-templates/v5.1.1/gridlist b/quicfire_tools/inputs/templates/v5.1.1/gridlist
similarity index 99%
rename from quicfire_tools/input-templates/v5.1.1/gridlist
rename to quicfire_tools/inputs/templates/v5.1.1/gridlist
index dfa168f..a55adba 100644
--- a/quicfire_tools/input-templates/v5.1.1/gridlist
+++ b/quicfire_tools/inputs/templates/v5.1.1/gridlist
@@ -92,4 +92,4 @@
        nonos=1 nfct=1 nonosold=1
        inonlocal=0 isubgridgas=0
        !st=1.0e-05
-/
\ No newline at end of file
+/
diff --git a/quicfire_tools/input-templates/latest/rasterorigin.txt b/quicfire_tools/inputs/templates/v5.1.1/rasterorigin.txt
similarity index 92%
rename from quicfire_tools/input-templates/latest/rasterorigin.txt
rename to quicfire_tools/inputs/templates/v5.1.1/rasterorigin.txt
index b0916ba..3549dca 100644
--- a/quicfire_tools/input-templates/latest/rasterorigin.txt
+++ b/quicfire_tools/inputs/templates/v5.1.1/rasterorigin.txt
@@ -4,4 +4,4 @@
 3752846.04249607
 742265.868913356
 3742846.04249607
-10000
\ No newline at end of file
+10000
diff --git a/quicfire_tools/input-templates/latest/sensor1.inp b/quicfire_tools/inputs/templates/v5.1.1/sensor1.inp
similarity index 96%
rename from quicfire_tools/input-templates/latest/sensor1.inp
rename to quicfire_tools/inputs/templates/v5.1.1/sensor1.inp
index 3713549..7d72798 100644
--- a/quicfire_tools/input-templates/latest/sensor1.inp
+++ b/quicfire_tools/inputs/templates/v5.1.1/sensor1.inp
@@ -1,4 +1,4 @@
-sensor1 !Site Name 
+sensor1 !Site Name
 0 !Upper level flag (1 = use this profile for upper level winds)
 50 !Upper level height (meters)
 1 !Site Coordinate Flag (1=QUIC, 2=UTM, 3=Lat/Lon)
diff --git a/quicfire_tools/input-templates/v5.2.3/QFire_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.2.3/QFire_Advanced_User_Inputs.inp
similarity index 98%
rename from quicfire_tools/input-templates/v5.2.3/QFire_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QFire_Advanced_User_Inputs.inp
index 5ff63c7..6c70b36 100755
--- a/quicfire_tools/input-templates/v5.2.3/QFire_Advanced_User_Inputs.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QFire_Advanced_User_Inputs.inp
@@ -1,14 +1,14 @@
-0.05			! N/A, fraction of the cells on fire that will launch a firebrand
-40.			! N/A, scaling factor of the radius represented by the firebrands launched
-1				! s, time step for the firebrands trajectory calculation
-10				! s, how often to launch firebrands
-500			! N/A, number of firebrands distributed over the landing area
-20.			! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
-50.			! N/A, min_b_value_coef
-0.75			! N/A, fb_frac_of_max_size
-180			! s, germination_delay
-5.				! N/A, fraction of the cell on fire (to scale w)
-50				! N/A, minimum number of ignitions via firebrands at a point
-100			! N/A, maximum number of ignitions via firebrands at a point
-0.523598		! rad, min_theta_value (pi/6)
-0.03        ! m, fb thickness
+0.05			! N/A, fraction of the cells on fire that will launch a firebrand
+40.			! N/A, scaling factor of the radius represented by the firebrands launched
+1				! s, time step for the firebrands trajectory calculation
+10				! s, how often to launch firebrands
+500			! N/A, number of firebrands distributed over the landing area
+20.			! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
+50.			! N/A, min_b_value_coef
+0.75			! N/A, fb_frac_of_max_size
+180			! s, germination_delay
+5.				! N/A, fraction of the cell on fire (to scale w)
+50				! N/A, minimum number of ignitions via firebrands at a point
+100			! N/A, maximum number of ignitions via firebrands at a point
+0.523598		! rad, min_theta_value (pi/6)
+0.03        ! m, fb thickness
diff --git a/quicfire_tools/input-templates/latest/QFire_Bldg_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.2.3/QFire_Bldg_Advanced_User_Inputs.inp
similarity index 100%
rename from quicfire_tools/input-templates/latest/QFire_Bldg_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QFire_Bldg_Advanced_User_Inputs.inp
diff --git a/quicfire_tools/input-templates/latest/QFire_Plume_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp
similarity index 92%
rename from quicfire_tools/input-templates/latest/QFire_Plume_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp
index a1d2b25..b0b462e 100755
--- a/quicfire_tools/input-templates/latest/QFire_Plume_Advanced_User_Inputs.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QFire_Plume_Advanced_User_Inputs.inp
@@ -1,16 +1,16 @@
-150000				! N/A, max number of plume at each time step
-0.1				! m/s, minimum vertical velocity of a plume. If wc is below minimum, the plume is eliminated (default 0.1 m/s)
-100				! m/s, maximum vertical velocity of a plume (default 100 m/s)
-0.1				! N/A, minimum speed ratio (plume vertical velocity/wind speed). If below, the plume is eliminated (default 0.1)
-0.					! 1/s2, brunt vaisala frequency squared
-1					! N/A creeping flag: 0 = off, 1 = on
-0					! Flag time step (0 = constant, 1 = adaptive)
-1              ! s, flag = 0: plumes time step plume, flag = 1: minimum plumes time step
-1              ! sor option (0 = reset lambda at each call, 1 = keep)
-10             ! alpha 2 - plume centerline
-2              ! alpha 2 - plume edges
-30.            ! deg, max angle between plumes to merging
-0.7            ! N/A, fraction of overlap of plumes for merging 
-1              ! N/A, which wplume-to-grid scheme: 0 = new, 1 = old
-10             ! N/A, number of points per edge for new scheme (only new scheme)
-1              ! N/A, w computation: 1 = max(w), 0 = sum(w^3)
+150000				! N/A, max number of plume at each time step
+0.1				! m/s, minimum vertical velocity of a plume. If wc is below minimum, the plume is eliminated (default 0.1 m/s)
+100				! m/s, maximum vertical velocity of a plume (default 100 m/s)
+0.1				! N/A, minimum speed ratio (plume vertical velocity/wind speed). If below, the plume is eliminated (default 0.1)
+0.					! 1/s2, brunt vaisala frequency squared
+1					! N/A creeping flag: 0 = off, 1 = on
+0					! Flag time step (0 = constant, 1 = adaptive)
+1              ! s, flag = 0: plumes time step plume, flag = 1: minimum plumes time step
+1              ! sor option (0 = reset lambda at each call, 1 = keep)
+10             ! alpha 2 - plume centerline
+2              ! alpha 2 - plume edges
+30.            ! deg, max angle between plumes to merging
+0.7            ! N/A, fraction of overlap of plumes for merging
+1              ! N/A, which wplume-to-grid scheme: 0 = new, 1 = old
+10             ! N/A, number of points per edge for new scheme (only new scheme)
+1              ! N/A, w computation: 1 = max(w), 0 = sum(w^3)
diff --git a/quicfire_tools/input-templates/latest/QFire_Pyvista.inp b/quicfire_tools/inputs/templates/v5.2.3/QFire_Pyvista.inp
similarity index 99%
rename from quicfire_tools/input-templates/latest/QFire_Pyvista.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QFire_Pyvista.inp
index d2a32d6..af7515e 100644
--- a/quicfire_tools/input-templates/latest/QFire_Pyvista.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QFire_Pyvista.inp
@@ -25,4 +25,3 @@ camOrbit=0
 windBarNormalized=none
 windBarBounds=[-10.0,10.0]
 compile=0 run=1 compiler='gfortran' compiler_mode='release' testcase='LineFire' ./adv_compile_and_run.sh
-
diff --git a/quicfire_tools/input-templates/v5.2.3/QP_buildout.inp b/quicfire_tools/inputs/templates/v5.2.3/QP_buildout.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.2.3/QP_buildout.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QP_buildout.inp
diff --git a/quicfire_tools/inputs/templates/v5.2.3/QUIC_fire.inp b/quicfire_tools/inputs/templates/v5.2.3/QUIC_fire.inp
new file mode 100644
index 0000000..552fa12
--- /dev/null
+++ b/quicfire_tools/inputs/templates/v5.2.3/QUIC_fire.inp
@@ -0,0 +1,42 @@
+$fire_flag         ! Fire flag: 1 = run fire; 0 = no fire
+$random_seed               ! Random number generator: -1: use time and date, any other integer > 0 is used as the seed
+! FIRE TIMES
+$time_now		    ! When the fire is ignited in Unix Epoch time (integer seconds since 1970/1/1 00:00:00). Must be greater or equal to the time of the first wind
+$sim_time			! Total simulation time for the fire [s]
+$fire_time_step		! time step for the fire simulation [s]
+$quic_time_step		! Number of fire time steps done before updating the quic wind field (integer, >= 1)
+$out_time_fire      ! After how many fire time steps to print out fire-related files (excluding emissions and radiation)
+$out_time_wind      ! After how many quic updates to print out wind-related files
+$out_time_emis_rad  ! After how many fire time steps to average emissions and radiation
+$out_time_wind_avg  ! After how many quic updates to print out averaged wind-related files
+! FIRE GRID
+$nz					! Number of vertical layers of fire grid cells (integer)
+$stretch_grid_flag  ! Vertical stretching flag: 0 = uniform dz, 1 = custom
+$stretch_grid_input ! m, dz
+! FILE PATH
+""
+1                   ! Fuel types are in separate files
+2                   ! File is stream (1) or headers (2)
+! FUEL
+$fuel_lines
+! IGNITION LOCATIONS
+$ignition_lines
+$ignitions_per_cell ! number of ignitions
+! FIREBRANDS
+$firebrand_flag		! 0 = off, 1 = on
+! OUTPUT FILES (formats depend on the grid type flag)
+$eng_to_atm_out		! Output gridded energy-to-atmosphere (3D fire grid + extra layers)
+$react_rate_out		! Output compressed array reaction rate (fire grid)
+$fuel_dens_out		! Output compressed array fuel density (fire grid)
+$QF_wind_out	    ! Output gridded wind (u,v,w,sigma) (3D fire grid)
+$QU_wind_inst_out	! Output gridded QU winds with fire effects, instantaneous (QUIC-URB grid)
+$QU_wind_avg_out		! Output gridded QU winds with fire effects, averaged (QUIC-URB grid)
+0					! Output plume trajectories (ONLY FOR DEBUG)
+$fuel_moist_out		! Output compressed array fuel moisture (fire grid)
+$mass_burnt_out		! Output vertically-integrated % mass burnt (fire grid)
+$firebrand_out		! Output trajectories firebrands
+$emissions_out		! Output compressed array emissions (fire grid)
+$radiation_out		! Output gridded thermal radiation (fire grid)
+$intensity_out      ! Output surface fire intensity at every fire time step
+! AUTOKILL
+$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
diff --git a/quicfire_tools/inputs/templates/v5.2.3/QU_TopoInputs.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_TopoInputs.inp
new file mode 100644
index 0000000..60b3ae4
--- /dev/null
+++ b/quicfire_tools/inputs/templates/v5.2.3/QU_TopoInputs.inp
@@ -0,0 +1,11 @@
+Specify file name for custom topo (full path)
+$filename
+$topo_lines
+$smoothing_method   ! N/A, smoothing method (0 = none (default for idealized terrain), 1 = blur, 2 = David's)
+$smoothing_passes   ! N/A, number of smoothing passes
+$sor_iterations     ! N/A, number of initial SOR iterations (only if fire is run)
+$sor_cycles         ! N/A, sor cycles
+$sor_relax          ! N/A, sor relaxation parameter (default for flat is 1.78, ignored if there is no terrain)
+0                   ! N/A, add slope flow correction (0 = no, 1 = yes)
+Command to run slope flow code (system specific)
+""
diff --git a/quicfire_tools/input-templates/latest/QU_buildings.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_buildings.inp
similarity index 96%
rename from quicfire_tools/input-templates/latest/QU_buildings.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_buildings.inp
index 6dd3867..db22cec 100755
--- a/quicfire_tools/input-templates/latest/QU_buildings.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QU_buildings.inp
@@ -1,4 +1,4 @@
-!QUIC 6.26
-0.1			!Wall roughness length (m)
-0			!Number of Buildings
-0			!Number of Polygon Building Nodes
+!QUIC 6.26
+0.1			!Wall roughness length (m)
+0			!Number of Buildings
+0			!Number of Polygon Building Nodes
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_fileoptions.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_fileoptions.inp
similarity index 86%
rename from quicfire_tools/input-templates/v5.2.3/QU_fileoptions.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_fileoptions.inp
index cd9678d..605458f 100755
--- a/quicfire_tools/input-templates/v5.2.3/QU_fileoptions.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QU_fileoptions.inp
@@ -1,6 +1,6 @@
-! QUIC 6.26 File Header
-2   !output data file format flag (1=ascii, 2=binary, 3=both)
-0   !flag to write out non-mass conserved initial field (uofield.dat) (1=write,0=no write)
-0   !flag to write out the file uosensorfield.dat, the initial sensor velocity field (1=write,0=no write)
-0   !flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure(1=write,0=no write)
-0   !flag to generate startup files (0 = no, 1 = yes)
\ No newline at end of file
+! QUIC 6.26 File Header
+2   !output data file format flag (1=ascii, 2=binary, 3=both)
+0   !flag to write out non-mass conserved initial field (uofield.dat) (1=write,0=no write)
+0   !flag to write out the file uosensorfield.dat, the initial sensor velocity field (1=write,0=no write)
+0   !flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure(1=write,0=no write)
+0   !flag to generate startup files (0 = no, 1 = yes)
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_landuse.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_landuse.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.2.3/QU_landuse.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_landuse.inp
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_metparams.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_metparams.inp
similarity index 58%
rename from quicfire_tools/input-templates/v5.1.1/QU_metparams.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_metparams.inp
index a41fb25..3f801e8 100644
--- a/quicfire_tools/input-templates/v5.1.1/QU_metparams.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QU_metparams.inp
@@ -1,7 +1,5 @@
 !QUIC 6.26
 0 !Met input flag (0=QUIC,1=WRF,2=ITT MM5,3=HOTMAC)
-1 !Number of measuring sites
+$num_sensors    !Number of measuring sites
 1 !Maximum size of data points profiles
-sensor1 !Site Name 
-!File name
-sensor1.inp
+$sensor_lines
diff --git a/quicfire_tools/input-templates/v5.2.3/QU_movingcoords.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_movingcoords.inp
similarity index 100%
rename from quicfire_tools/input-templates/v5.2.3/QU_movingcoords.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_movingcoords.inp
diff --git a/quicfire_tools/input-templates/latest/QU_simparams.inp b/quicfire_tools/inputs/templates/v5.2.3/QU_simparams.inp
similarity index 96%
rename from quicfire_tools/input-templates/latest/QU_simparams.inp
rename to quicfire_tools/inputs/templates/v5.2.3/QU_simparams.inp
index 400ed26..ae1ee5e 100755
--- a/quicfire_tools/input-templates/latest/QU_simparams.inp
+++ b/quicfire_tools/inputs/templates/v5.2.3/QU_simparams.inp
@@ -1,67 +1,67 @@
-!QUIC 6.26
-$nx !nx - Domain Length(X) Grid Cells
-$ny !ny - Domain Width(Y) Grid Cells
-32 ! NZ - domain height(Z) grid cells
-$dx ! DX [m]
-$dy ! DY [m]
-3 !Vertical stretching flag (0 = uniform, 1 = custom, 2 = parabolic Z, 3 = parabolic DZ, 4 = exponential)
-1.000000 ! Surface DZ [m]
-5 ! Number of uniform surface cells
-! DZ array [m]
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.078399
-1.313596
-1.705591
-2.254385
-2.959976
-3.822366
-4.841553
-6.017539
-7.350323
-8.839905
-10.486285
-12.289463
-14.249439
-16.366213
-18.639786
-21.070156
-23.657325
-26.401292
-29.302056
-32.354352
-35.354352
-38.354352
-41.354352
-44.354352
-47.354352
-50.354352
-53.354352
-1 ! Number of time increments
-0 ! UTC conversion [hours]
-! Begining of time step in Unix epoch time [integer seconds since 1970/1/1 00:00:00]
-$timenow
-2 ! Rooftop flag (0 = none, 1 = log profile, 2 = vortex)
-3 ! Upwind cavity flag (0 = none, 1 = Rockle, 2 = MVP, 3 = HMVP)
-4 ! Street canyon flag (0 = none, 1 = Roeckle, 2 = CPB, 3 = exp. param. PKK, 4 = Roeckle w/ Fackrel)
-1 ! Street intersection flag (0 = off, 1 = on)
-4 ! Wake flag (0 = none, 1 = Rockle, 2 = modified Rockle, 3 = area scaled)
-1 ! Sidewall flag (0 = off, 1 = on)
-1 ! Individual tree wake flag
-2 ! Canopy flag (0 = off, 1 = Cionco w/o wakes, 2 = Cionco w/ wakes)
-1 ! Season flag (1 = Summer, 2 = Winter, 3 = Transition)
-10 ! Maximum number of iterations
-3 ! Residual reduction (Orders of Magnitude)
-0 ! Use diffusion algorithm (0 = off, 1 = on)
-20 ! Number of diffusion iterations
-0 ! Domain rotation relative to true north [degrees] (cw = +)
-0.0 ! UTMX of domain origin [m]
-0.0 ! UTMY of domain origin [m]
-1 ! UTM zone
-17 ! UTM zone leter (1 = A, 2 = B, etc.)
-0 ! QUIC-CFD Flag
-0 ! Explosive building damage flag (1 = on)
-0 ! Building Array Flag (1 = on)
+!QUIC 6.26
+$nx !nx - Domain Length(X) Grid Cells
+$ny !ny - Domain Width(Y) Grid Cells
+32 ! NZ - domain height(Z) grid cells
+$dx ! DX [m]
+$dy ! DY [m]
+3 !Vertical stretching flag (0 = uniform, 1 = custom, 2 = parabolic Z, 3 = parabolic DZ, 4 = exponential)
+1.000000 ! Surface DZ [m]
+5 ! Number of uniform surface cells
+! DZ array [m]
+1.000000
+1.000000
+1.000000
+1.000000
+1.000000
+1.078399
+1.313596
+1.705591
+2.254385
+2.959976
+3.822366
+4.841553
+6.017539
+7.350323
+8.839905
+10.486285
+12.289463
+14.249439
+16.366213
+18.639786
+21.070156
+23.657325
+26.401292
+29.302056
+32.354352
+35.354352
+38.354352
+41.354352
+44.354352
+47.354352
+50.354352
+53.354352
+1 ! Number of time increments
+0 ! UTC conversion [hours]
+! Begining of time step in Unix epoch time [integer seconds since 1970/1/1 00:00:00]
+$timenow
+2 ! Rooftop flag (0 = none, 1 = log profile, 2 = vortex)
+3 ! Upwind cavity flag (0 = none, 1 = Rockle, 2 = MVP, 3 = HMVP)
+4 ! Street canyon flag (0 = none, 1 = Roeckle, 2 = CPB, 3 = exp. param. PKK, 4 = Roeckle w/ Fackrel)
+1 ! Street intersection flag (0 = off, 1 = on)
+4 ! Wake flag (0 = none, 1 = Rockle, 2 = modified Rockle, 3 = area scaled)
+1 ! Sidewall flag (0 = off, 1 = on)
+1 ! Individual tree wake flag
+2 ! Canopy flag (0 = off, 1 = Cionco w/o wakes, 2 = Cionco w/ wakes)
+1 ! Season flag (1 = Summer, 2 = Winter, 3 = Transition)
+10 ! Maximum number of iterations
+3 ! Residual reduction (Orders of Magnitude)
+0 ! Use diffusion algorithm (0 = off, 1 = on)
+20 ! Number of diffusion iterations
+0 ! Domain rotation relative to true north [degrees] (cw = +)
+0.0 ! UTMX of domain origin [m]
+0.0 ! UTMY of domain origin [m]
+1 ! UTM zone
+17 ! UTM zone leter (1 = A, 2 = B, etc.)
+0 ! QUIC-CFD Flag
+0 ! Explosive building damage flag (1 = on)
+0 ! Building Array Flag (1 = on)
diff --git a/quicfire_tools/inputs/templates/v5.2.3/Runtime_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/v5.2.3/Runtime_Advanced_User_Inputs.inp
new file mode 100755
index 0000000..1083241
--- /dev/null
+++ b/quicfire_tools/inputs/templates/v5.2.3/Runtime_Advanced_User_Inputs.inp
@@ -0,0 +1,2 @@
+$num_cpus  ! max number of cpu threads used
+$use_acw  ! Use Adaptive Computation Window (0=Disabled 1=Enabled)
diff --git a/quicfire_tools/input-templates/latest/gridlist b/quicfire_tools/inputs/templates/v5.2.3/gridlist
similarity index 86%
rename from quicfire_tools/input-templates/latest/gridlist
rename to quicfire_tools/inputs/templates/v5.2.3/gridlist
index 4facaff..4c76e52 100644
--- a/quicfire_tools/input-templates/latest/gridlist
+++ b/quicfire_tools/inputs/templates/v5.2.3/gridlist
@@ -1,5 +1,5 @@
 n=$nx
-m=$ny 
+m=$ny
 l=$nz
 aa1=$dz
 nv=8
diff --git a/quicfire_tools/input-templates/v5.1.1/rasterorigin.txt b/quicfire_tools/inputs/templates/v5.2.3/rasterorigin.txt
similarity index 92%
rename from quicfire_tools/input-templates/v5.1.1/rasterorigin.txt
rename to quicfire_tools/inputs/templates/v5.2.3/rasterorigin.txt
index b0916ba..3549dca 100644
--- a/quicfire_tools/input-templates/v5.1.1/rasterorigin.txt
+++ b/quicfire_tools/inputs/templates/v5.2.3/rasterorigin.txt
@@ -4,4 +4,4 @@
 3752846.04249607
 742265.868913356
 3742846.04249607
-10000
\ No newline at end of file
+10000
diff --git a/quicfire_tools/inputs/templates/v5.2.3/sensor1.inp b/quicfire_tools/inputs/templates/v5.2.3/sensor1.inp
new file mode 100644
index 0000000..82c0949
--- /dev/null
+++ b/quicfire_tools/inputs/templates/v5.2.3/sensor1.inp
@@ -0,0 +1,7 @@
+sensor1 !Site Name
+0 !Upper level flag (1 = use this profile for upper level winds)
+50 !Upper level height (meters)
+1 !Site Coordinate Flag (1=QUIC, 2=UTM, 3=Lat/Lon)
+1 !X coordinate (meters)
+1 !Y coordinate (meters)
+$wind_lines
diff --git a/quicfire_tools/outputs.py b/quicfire_tools/outputs.py
index 931ab4e..0a23f5e 100644
--- a/quicfire_tools/outputs.py
+++ b/quicfire_tools/outputs.py
@@ -7,15 +7,15 @@
 import re
 from pathlib import Path
 
-# Internal imports
-from quicfire_tools.parameters import SimulationParameters
-
 # External imports
 import zarr
 import numpy as np
 import dask.array as da
 from numpy import ndarray
 
+# Internal imports
+from quicfire_tools.parameters import SimulationParameters
+
 FUELS_OUTPUTS = {
     "fire-energy_to_atmos": {
         "file_format": "compressed",
@@ -24,7 +24,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "Energy released to the atmosphere that generates "
-                       "buoyant plumes",
+        "buoyant plumes",
         "units": "kW",
     },
     "fire-reaction_rate": {
@@ -70,7 +70,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "2D file containing the percentage of mass burnt for each (i,j) "
-                       "location on the fire grid (vertically integrated)",
+        "location on the fire grid (vertically integrated)",
         "units": "%",
     },
 }
@@ -82,7 +82,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "The output shows the thermal flux to skin of a person "
-                       "collocated with fuel (for health effects).",
+        "collocated with fuel (for health effects).",
         "units": "(kW/m^2)^(4/3)s",
     },
     "thermalradiation": {
@@ -92,7 +92,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "The output shows the thermal flux to skin of a person "
-                       "collocated with fuel (for health effects).",
+        "collocated with fuel (for health effects).",
         "units": "kW/m^2",
     },
 }
@@ -133,7 +133,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "Mass of CO emitted between two emission file output"
-                       "times in grams",
+        "times in grams",
         "units": "g",
     },
     "pm-emissions": {
@@ -143,7 +143,7 @@
         "delimiter": "-",
         "extension": ".bin",
         "description": "Mass of PM2.5 emitted between two emission file output"
-                       "times in grams",
+        "times in grams",
         "units": "g",
     },
 }
@@ -156,9 +156,18 @@
 
 
 class OutputFile:
-    def __init__(self, name: str, file_format: str, shape: tuple,
-                 grid: str, delimiter: str, extension: str,
-                 description: str, units: str, index_map=None):
+    def __init__(
+        self,
+        name: str,
+        file_format: str,
+        shape: tuple,
+        grid: str,
+        delimiter: str,
+        extension: str,
+        description: str,
+        units: str,
+        index_map=None,
+    ):
         self.name = name
         self.file_format = file_format
         self.shape = shape
@@ -170,14 +179,15 @@ def __init__(self, name: str, file_format: str, shape: tuple,
         self.times = []  # List of times corresponding to the timesteps
         self.filepaths = []  # List of file paths for each timestep
         function_mappings = {
-            'gridded': _process_gridded_bin,
-            'compressed': _process_compressed_bin
+            "gridded": _process_gridded_bin,
+            "compressed": _process_compressed_bin,
         }
         self._output_function = function_mappings.get(self.file_format)
         if self._output_function is None:
             raise ValueError(f"Unknown output format: {self.file_format}")
-        self._compressed_index_map = index_map if (self.file_format ==
-                                                   'compressed') else None
+        self._compressed_index_map = (
+            index_map if (self.file_format == "compressed") else None
+        )
 
     def add_timestep(self, time: float, filepath: str) -> None:
         """Add a new timestep to the OutputFile."""
@@ -202,7 +212,9 @@ def _validate_timestep(self, timestep: int | list[int] | None) -> None:
         else:
             raise TypeError(f"Invalid timestep type: {type(timestep)}")
 
-    def _select_files_based_on_timestep(self, timestep: int | list[int] | None) -> list[Path]:
+    def _select_files_based_on_timestep(
+        self, timestep: int | list[int] | None
+    ) -> list[Path]:
         """Return files selected based on timestep."""
         if timestep is None:
             return self.filepaths
@@ -212,9 +224,9 @@ def _select_files_based_on_timestep(self, timestep: int | list[int] | None) -> l
 
     def _get_single_timestep(self, output_file: Path) -> np.ndarray:
         """Return a numpy array for the given output file."""
-        return self._output_function(output_file,
-                                     self.shape,
-                                     self._compressed_index_map)
+        return self._output_function(
+            output_file, self.shape, self._compressed_index_map
+        )
 
     def _get_multiple_timesteps(self, output_files: list[Path]) -> np.ndarray:
         """Return a numpy array for the given output files."""
@@ -231,8 +243,9 @@ class SimulationOutputs:
     zarr file.
     """
 
-    def __init__(self, output_directory: Path | str,
-                 params: SimulationParameters) -> None:
+    def __init__(
+        self, output_directory: Path | str, params: SimulationParameters
+    ) -> None:
         # Convert to Path and resolve
         output_directory = Path(output_directory).resolve()
 
@@ -294,7 +307,7 @@ def _build_output_files_map(self):
                     extension=attributes["extension"],
                     description=attributes["description"],
                     units=attributes["units"],
-                    index_map=self._fire_indexes
+                    index_map=self._fire_indexes,
                 )
 
                 # Populate the output file with timesteps and filepaths
@@ -324,13 +337,13 @@ def _get_output_shape(self, name, attrs) -> tuple:
     @staticmethod
     def _get_output_file_time(output, fpath) -> int:
         """Get the time of the output file."""
-        delimiter = OUTPUTS_MAP[output]['delimiter']
-        extension = OUTPUTS_MAP[output]['extension']
+        delimiter = OUTPUTS_MAP[output]["delimiter"]
+        extension = OUTPUTS_MAP[output]["extension"]
 
         try:
-            if delimiter == '':
+            if delimiter == "":
                 # Split on first numeric component
-                name_parts = re.split(r'(\d+)', fpath.name)
+                name_parts = re.split(r"(\d+)", fpath.name)
                 file_time = name_parts[1]
             else:
                 file_time = fpath.name.split(delimiter)[-1].split(extension)[0]
@@ -383,8 +396,9 @@ def get_output(self, key):
         """Return a list of times for a given output key."""
         return self.outputs[key]
 
-    def to_numpy(self, key: str | OutputFile,
-                 timestep: None | int | list[int] = None) -> np.ndarray:
+    def to_numpy(
+        self, key: str | OutputFile, timestep: None | int | list[int] = None
+    ) -> np.ndarray:
         """Return a numpy array for the given output and timestep(s)."""
         output = self._validate_output(key)
         return output.to_numpy(timestep)
@@ -395,19 +409,25 @@ def _validate_output(self, output: str | OutputFile) -> OutputFile:
             try:
                 output = self.outputs[output]
             except KeyError:
-                raise ValueError(f"{output} is not a valid output. Valid "
-                                 f"outputs are: {self.list_available_outputs()}")
+                raise ValueError(
+                    f"{output} is not a valid output. Valid "
+                    f"outputs are: {self.list_available_outputs()}"
+                )
         elif isinstance(output, OutputFile):
             if output not in self.outputs.values():
-                raise ValueError(f"{output} is not a valid output. Valid "
-                                 f"outputs are: {self.list_available_outputs()}")
+                raise ValueError(
+                    f"{output} is not a valid output. Valid "
+                    f"outputs are: {self.list_available_outputs()}"
+                )
         else:
-            raise TypeError(f"output must be a string or OutputFile object. "
-                            f"Got {type(output)}")
+            raise TypeError(
+                f"output must be a string or OutputFile object. " f"Got {type(output)}"
+            )
         return output
 
-    def to_dask(self, key: str | OutputFile,
-                timestep: None | int | list[int] = None) -> np.ndarray:
+    def to_dask(
+        self, key: str | OutputFile, timestep: None | int | list[int] = None
+    ) -> np.ndarray:
         """Return a dask array for the given output and timestep(s)."""
         output = self._validate_output(key)
 
@@ -443,11 +463,9 @@ def to_zarr(self, fpath: Path, outputs: str | list[str] = None):
             shape = (len(output.times), *output.shape)
             chunks = [1 if i == 0 else shape[i] for i in range(len(shape))]
             zarr_dataset = zarr_file.create_dataset(
-                output_name,
-                shape=shape,
-                chunks=chunks,
-                dtype=float)
-            zarr_dataset.attrs['_ARRAY_DIMENSIONS'] = ["time", "y", "x", "z"]
+                output_name, shape=shape, chunks=chunks, dtype=float
+            )
+            zarr_dataset.attrs["_ARRAY_DIMENSIONS"] = ["time", "y", "x", "z"]
 
             # Write each timestep to the output's zarr dataset
             for time_step in range(len(output.times)):
@@ -492,16 +510,18 @@ def _process_compressed_bin(filename, dim_yxz, *args) -> ndarray:
         If the index map (fire_indexes) is not provided in the arguments.
     ValueError
         If the index map (fire_indexes) is None.
-   """
+    """
     # TODO: Rename fire_indexes to something more generic
     try:
         fire_indexes = args[0]
     except IndexError:
-        raise ValueError("fire_indexes must be provided when processing "
-                         "compressed .bin files.")
+        raise ValueError(
+            "fire_indexes must be provided when processing " "compressed .bin files."
+        )
     if fire_indexes is None:
-        raise ValueError("fire_indexes must be provided when processing "
-                         "compressed .bin files.")
+        raise ValueError(
+            "fire_indexes must be provided when processing " "compressed .bin files."
+        )
 
     # Initialize the 3D array
     full_3d_array = np.zeros(dim_yxz, dtype=np.float32)
@@ -511,9 +531,7 @@ def _process_compressed_bin(filename, dim_yxz, *args) -> ndarray:
         np.fromfile(fid, dtype=np.int32, count=1)
 
         # Read in the sparse values
-        sparse_values = np.fromfile(
-            fid, dtype=np.float32, count=fire_indexes.shape[0]
-        )
+        sparse_values = np.fromfile(fid, dtype=np.float32, count=fire_indexes.shape[0])
 
         # Map indices of the sparse data to the indices of the dense array
         indices = (
diff --git a/quicfire_tools/parameters.py b/quicfire_tools/parameters.py
index 0db9797..261960d 100644
--- a/quicfire_tools/parameters.py
+++ b/quicfire_tools/parameters.py
@@ -1,5 +1,5 @@
 # External Imports
-from pydantic import BaseModel, Field, field_validator
+from pydantic import BaseModel, Field
 
 
 class SimulationParameters(BaseModel):
@@ -64,22 +64,39 @@ def to_dict(self):
         return self.model_dump()
 
     @classmethod
-    def from_fastfuels(cls, zarray, wind_speed, wind_direction, sim_time, auto_kill, num_cpus, fuel_flag, ignition_flag, output_time, topo_flag, fuel_density, fuel_moisture, fuel_height):
-        return cls(nx=zarray.attrs["dx"],
-                   ny=zarray.attrs["dy"],
-                   nz=zarray.attrs["dz"],
-                   dx=zarray.attrs["dx"],
-                   dy=zarray.attrs["dy"],
-                   dz=zarray.attrs["dz"],
-                   wind_speed=wind_speed,
-                   wind_direction=wind_direction,
-                   sim_time=sim_time,
-                   auto_kill=auto_kill,
-                   num_cpus=num_cpus,
-                   fuel_flag=fuel_flag,
-                   ignition_flag=ignition_flag,
-                   output_time=output_time,
-                   topo_flag=topo_flag,
-                   fuel_density=fuel_density,
-                   fuel_moisture=fuel_moisture,
-                   fuel_height=fuel_height)
+    def from_fastfuels(
+        cls,
+        zarray,
+        wind_speed,
+        wind_direction,
+        sim_time,
+        auto_kill,
+        num_cpus,
+        fuel_flag,
+        ignition_flag,
+        output_time,
+        topo_flag,
+        fuel_density,
+        fuel_moisture,
+        fuel_height,
+    ):
+        return cls(
+            nx=zarray.attrs["dx"],
+            ny=zarray.attrs["dy"],
+            nz=zarray.attrs["dz"],
+            dx=zarray.attrs["dx"],
+            dy=zarray.attrs["dy"],
+            dz=zarray.attrs["dz"],
+            wind_speed=wind_speed,
+            wind_direction=wind_direction,
+            sim_time=sim_time,
+            auto_kill=auto_kill,
+            num_cpus=num_cpus,
+            fuel_flag=fuel_flag,
+            ignition_flag=ignition_flag,
+            output_time=output_time,
+            topo_flag=topo_flag,
+            fuel_density=fuel_density,
+            fuel_moisture=fuel_moisture,
+            fuel_height=fuel_height,
+        )
diff --git a/quicfire_tools/topography.py b/quicfire_tools/topography.py
new file mode 100644
index 0000000..3a547ea
--- /dev/null
+++ b/quicfire_tools/topography.py
@@ -0,0 +1,148 @@
+"""
+QUIC-Fire Tools Topography Model
+"""
+from __future__ import annotations
+
+# Core Imports
+from enum import Enum
+
+# External Imports
+from typing import Literal
+
+from pydantic import BaseModel, Field, PositiveFloat, PositiveInt, SerializeAsAny
+
+
+class TopoSources(int, Enum):
+    flat = 0
+    gaussian_hill = 1
+    hill_pass = 2
+    slope_mesa = 3
+    canyon = 4
+    custom = 5
+    half_circle = 6
+    sinusoid = 7
+    cos_hill = 8
+    QP_elevation_bin = 9
+    terrainOutput_txt = 10
+    terrain_dat = 11
+
+
+class TopoType(BaseModel):
+    topo_flag: SerializeAsAny[TopoSources]
+
+    def __str__(self):
+        return (
+            f"{self.topo_flag.value}\t\t! N/A, "
+            f"topo flag: 0 = flat, 1 = Gaussian hill, "
+            f"2 = hill pass, 3 = slope mesa, 4 = canyon, "
+            f"5 = custom, 6 = half circle, 7 = sinusoid, "
+            f"8 = cos hill, 9 = QP_elevation.inp, "
+            f"10 = terrainOutput.txt (ARA), "
+            f"11 = terrain.dat (firetec)"
+        )
+
+
+class GaussianHillTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(1)
+    x_hilltop: PositiveInt
+    y_hilltop: PositiveInt
+    elevation_max: PositiveInt
+    elevation_std: PositiveFloat
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = (
+            f"\n{self.x_hilltop}\t! m, x-center\n"
+            f"{self.y_hilltop}\t! m, y-center\n"
+            f"{self.elevation_max}\t! m, max height\n"
+            f"{self.elevation_std}\t! m, std"
+        )
+        return flag_line + params
+
+
+class HillPassTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(2)
+    max_height: PositiveInt
+    location_param: PositiveFloat
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = (
+            f"{self.max_height}\t! m, max height\n"
+            f"{self.location_param}\t! m, location parameter"
+        )
+        return flag_line + params
+
+
+class SlopeMesaTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(3)
+    slope_axis: Literal[0, 1]
+    slope_value: PositiveFloat
+    flat_fraction: float = Field(ge=0, le=1)
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = (
+            f"{self.slope_axis}\t! N/A, slope axis (0 = x, 1 = y)\n"
+            f"{self.slope_value}\t! N/A, slope val (dh/dx)\n"
+            f"{self.flat_fraction}\t! N/A, fraction of domain that is flat"
+        )
+        return flag_line + params
+
+
+class CanyonTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(4)
+    x_start: PositiveInt
+    y_center: PositiveInt
+    slope_value: PositiveFloat
+    canyon_std: PositiveFloat
+    vertical_offset: PositiveFloat
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = (
+            f"{self.x_start}\t! m, x-start of canyon on slope\n"
+            f"{self.y_center}\t! m, y-center of canyon"
+            f"{self.slope_value}\t! N/A, slope\n"
+            f"{self.canyon_std}\t! m, std\n"
+            f"{self.vertical_offset}\t! m, height offset of the bottom of the canyon"
+        )
+        return flag_line + params
+
+
+class HalfCircleTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(6)
+    x_location: PositiveInt
+    y_location: PositiveInt
+    radius: PositiveFloat
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = (
+            f"{self.x_location}\t! m, x-location\n"
+            f"{self.y_location}\t! m, y-location\n"
+            f"{self.radius}\t! m, radius of half circle"
+        )
+        return flag_line + params
+
+
+class SinusoidTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(7)
+    period: PositiveFloat
+    amplitude: PositiveFloat
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = f"{self.period}\t! m, mean\n" f"{self.amplitude}\t! m, amplitude"
+        return flag_line + params
+
+
+class CosHillTopo(TopoType):
+    topo_flag: SerializeAsAny[TopoSources] = TopoSources(8)
+    aspect: PositiveFloat
+    height: PositiveInt
+
+    def __str__(self):
+        flag_line = super().__str__()
+        params = f"{self.aspect}\t! m, mean\n" f"{self.height}\t! m, height"
+        return flag_line + params
diff --git a/quicfire_tools/utils.py b/quicfire_tools/utils.py
new file mode 100644
index 0000000..102e755
--- /dev/null
+++ b/quicfire_tools/utils.py
@@ -0,0 +1,69 @@
+"""
+Utility functions for quicfire-tools.
+"""
+
+import numpy as np
+
+
+def compute_parabolic_stretched_grid(
+    dz_surf: float, n_surf_cell: int, n_tot_cells: int, domain_height: float
+) -> np.ndarray:
+    """
+    Generate a vertical grid spacing array with parabolic stretching.
+
+    Parameters
+    ----------
+    dz_surf : float
+        Vertical cell spacing at the surface, typically about 1 m.
+    n_surf_cell : int
+        Number of cells with constant vertical cell spacing (dz_surf) at the
+        surface.
+    n_tot_cells : int
+        Total number of vertical cells in the grid. Typically ranges between
+        20 and 30.
+    domain_height : float
+        Total height of the grid. For flat terrain without smoke transport
+        concerns, it is usually around 100 m. For terrain with features, it
+        should be approximately 3 times the height of the tallest feature.
+
+    Returns
+    -------
+    np.ndarray
+        Array representing the vertical grid spacing (dz) values for the
+        simulation.
+
+    Notes
+    -----
+    The function computes a parabolic stretching of the vertical grid spacings.
+    The grid consists of a specified number of cells with uniform spacing at
+    the surface, followed by cells with spacings determined by a parabolic
+    formula until the total domain height is achieved.
+    """
+    dzmax_high = domain_height - dz_surf * n_surf_cell
+    dzmax_low = 0
+
+    dz = np.ones(n_tot_cells) * dz_surf
+
+    while True:
+        dzmax = 0.5 * (dzmax_low + dzmax_high)
+
+        c1 = (dzmax - dz_surf) / ((n_tot_cells - n_surf_cell) ** 2)
+        c2 = -2.0 * c1 * n_surf_cell
+        c3 = dz_surf + c1 * n_surf_cell**2
+
+        # Apply parabolic formula for cells beyond the surface cells
+        kreal = np.arange(n_surf_cell, n_tot_cells, dtype=float)
+        dz[n_surf_cell:] = (c1 * kreal**2) + (c2 * kreal) + c3
+
+        zmax_temp = np.sum(dz)
+
+        if abs(domain_height - zmax_temp) < 0.001:
+            break
+        elif zmax_temp > domain_height:
+            dzmax_high = dzmax
+        else:
+            dzmax_low = dzmax
+
+    dz[-1] += domain_height - np.sum(dz)
+
+    return dz
diff --git a/requirements/docs.txt b/requirements/docs.txt
index d27ee82..4a30805 100644
--- a/requirements/docs.txt
+++ b/requirements/docs.txt
@@ -1,2 +1,2 @@
 mkdocstrings
-mkdocstrings-python
\ No newline at end of file
+mkdocstrings-python
diff --git a/requirements/readme.md b/requirements/readme.md
index a6b833a..c08b860 100644
--- a/requirements/readme.md
+++ b/requirements/readme.md
@@ -16,4 +16,3 @@ Requirements for building documentation.
 
 Requirements for testing with different Python versions. Uses conda environment
 yaml files.
-
diff --git a/requirements/requirements.yml b/requirements/requirements.yml
index 9400144..7a182ec 100644
--- a/requirements/requirements.yml
+++ b/requirements/requirements.yml
@@ -11,4 +11,4 @@ dependencies:
   - pandas
   - ca-certificates
   - openssl
-  - dask
\ No newline at end of file
+  - dask
diff --git a/setup.py b/setup.py
index 62c04e8..11313ba 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,3 @@
-import os
 import requests
 from setuptools import find_packages, setup
 
@@ -23,9 +22,7 @@ def get_version():
 VERSION = get_version()
 LICENSE = "MIT"
 URL = "https://github.com/silvxlabs/quicfire-tools"
-PROJECT_URLS = {
-    "Bug Tracker": f"{URL}/issues"
-}
+PROJECT_URLS = {"Bug Tracker": f"{URL}/issues"}
 
 setup(
     name=NAME,
@@ -36,20 +33,22 @@ def get_version():
     license=LICENSE,
     url=URL,
     project_urls=PROJECT_URLS,
-    classifiers=["Intended Audience :: Developers",
-                 "Intended Audience :: Science/Research",
-                 "License :: OSI Approved :: MIT License",
-                 "Operating System :: OS Independent",
-                 "Programming Language :: Python :: 3",
-                 "Programming Language :: Python :: 3.8",
-                 "Programming Language :: Python :: 3.9",
-                 "Programming Language :: Python :: 3.10",
-                 "Programming Language :: Python :: 3.11",
-                 "Topic :: Scientific/Engineering", ],
+    classifiers=[
+        "Intended Audience :: Developers",
+        "Intended Audience :: Science/Research",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Topic :: Scientific/Engineering",
+    ],
     package_dir={"": "."},
     packages=find_packages(exclude=["docs", "tests"]),
     package_data={
-        'quicfire_tools': ['input-templates/*/*'],
+        "quicfire_tools": ["templates/*/*"],
     },
     include_package_data=True,
     install_requires=[


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 039d0a2..c0511c0 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +1,2 @@
 [pytest]
-log_cli = true
\ No newline at end of file
+log_cli = true
diff --git a/quicfire_tools/input-templates/latest/QUIC_fire.inp b/quicfire_tools/input-templates/latest/QUIC_fire.inp
deleted file mode 100644
index 07bfe9a..0000000
--- a/quicfire_tools/input-templates/latest/QUIC_fire.inp
+++ /dev/null
@@ -1,43 +0,0 @@
-1					! Fire flag: 1 = run fire; 0 = no fire
--1				    ! Random number generator: -1: use time and date, any other integer > 0 is used as the seed
-! FIRE TIMES
-$timenow		    ! When the fire is ignited in Unix Epoch time (integer seconds since 1970/1/1 00:00:00). Must be greater or equal to the time of the first wind
-$sim_time			! Total simulation time for the fire [s]
-1					! time step for the fire simulation [s]
-1					! Number of fire time steps done before updating the quic wind field (integer, >= 1)
-$output_time		! After how many fire time steps to print out fire-related files (excluding emissions and radiation)
-$output_time		! After how many quic updates to print out wind-related files
-$output_time		! After how many fire time steps to average emissions and radiation
-$output_time		! After how many quic updates to print out averaged wind-related files
-! FIRE GRID
-$nz					! Number of vertical layers of fire grid cells (integer)
-0					! Vertical stretching flag: 0 = uniform dz, 1 = custom
-$dz                 ! m, dz
-! FILE PATH
-""
-1                   ! Fuel types are in separate files
-2                   ! File is stream (1) or headers (2)
-! FUEL
-$fuel_flag		    ! fuel density flag: 1 = uniform; 2 = provided thru QF_FuelDensity.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)    $fuel_density
-$fuel_flag		    ! fuel moisture flag: 1 = uniform; 2 = provided thru QF_FuelMoisture.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)  $fuel_moisture $fuel_height_flag
-! IGNITION LOCATIONS
-$ignition_flag      ! 1 = rectangle, 2 = square ring, 3 = circular ring, 4 = file (QF_Ignitions.inp), 5 = time-dependent ignitions (QF_IgnitionPattern.inp), 6 = ignite.dat (firetech)      $ignition_locations
-2                   ! number of ignitions
-! FIREBRANDS
-0				    ! 0 = off, 1 = on
-! OUTPUT FILES (formats depend on the grid type flag)
-1					! Output gridded energy-to-atmosphere (3D fire grid + extra layers)
-0					! Output compressed array reaction rate (fire grid)
-1					! Output compressed array fuel density (fire grid)
-1					! Output gridded wind (u,v,w,sigma) (3D fire grid)
-1					! Output gridded QU winds with fire effects, instantaneous (QUIC-URB grid)
-0					! Output gridded QU winds with fire effects, averaged (QUIC-URB grid)
-0					! Output plume trajectories (ONLY FOR DEBUG)
-1					! Output compressed array fuel moisture (fire grid)
-1					! Output vertically-integrated % mass burnt (fire grid)
-0					! Output trajectories firebrands
-0					! Output compressed array emissions (fire grid)
-0					! Output gridded thermal radiation (fire grid)
-0                   ! Output surface fire intensity at every fire time step
-! AUTOKILL
-$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
\ No newline at end of file
diff --git a/quicfire_tools/input-templates/latest/QU_TopoInputs.inp b/quicfire_tools/input-templates/latest/QU_TopoInputs.inp
deleted file mode 100644
index 87c1cfc..0000000
--- a/quicfire_tools/input-templates/latest/QU_TopoInputs.inp
+++ /dev/null
@@ -1,11 +0,0 @@
-Specify file name for custom topo (full path)
-"topo.dat"
-$topo_flag     ! N/A, topo flag: 0 = flat, 1 = Gaussian hill, 2 = hill pass, 3 = slope mesa, 4 = canyon, 5 = custom, 6 = half circle, 7 = sinusoid, 8 = cos hill, 9 = QP_elevation.inp, 10 = terrainOutput.txt (ARA), 11 = terrain.dat (firetec)
-1     ! N/A, smoothing method (0 = none (default for idealized terrain), 1 = blur, 2 = David's)
-200     ! N/A, number of smoothing passes
-200   ! N/A, number of initial SOR iterations (only if fire is run)
-1     ! N/A, sor cycles
-0.7   ! N/A, sor relaxation parameter (default for flat is 1.78, ignored if there is no terrain)
-0     ! N/A, add slope flow correction (0 = no, 1 = yes)
-Command to run slope flow code (system specific)
-""
diff --git a/quicfire_tools/inputs/templates/latest/QFire_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/latest/QFire_Advanced_User_Inputs.inp
new file mode 100755
index 0000000..a9c7cb9
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QFire_Advanced_User_Inputs.inp
@@ -0,0 +1,14 @@
+$fraction_cells_launch_firebrands       ! N/A, fraction of the cells on fire that will launch a firebrand
+$firebrand_radius_scale_factor          ! N/A, scaling factor of the radius represented by the firebrands launched
+$firebrand_trajectory_time_step         ! s, time step for the firebrands trajectory calculation
+$firebrand_launch_interval              ! s, how often to launch firebrands
+$firebrands_per_deposition              ! N/A, number of firebrands distributed over the landing area
+$firebrand_area_ratio                   ! N/A, FB_FRACTION_LAUNCHED_to_RT_ratio
+$minimum_burn_rate_coefficient          ! N/A, min_b_value_coef
+$max_firebrand_thickness_fraction       ! N/A, fb_frac_of_max_size
+$firebrand_germination_delay            ! s, germination_delay
+$vertical_velocity_scale_factor         ! N/A, fraction of the cell on fire (to scale w)
+$minimum_firebrand_ignitions            ! N/A, minimum number of ignitions via firebrands at a point
+$maximum_firebrand_ignitions            ! N/A, maximum number of ignitions via firebrands at a point
+$minimum_landing_angle                  ! rad, min_theta_value (pi/6)
+$maximum_firebrand_thickness            ! m, fb thickness
diff --git a/quicfire_tools/inputs/templates/latest/QFire_Bldg_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/latest/QFire_Bldg_Advanced_User_Inputs.inp
new file mode 100755
index 0000000..b2725a5
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QFire_Bldg_Advanced_User_Inputs.inp
@@ -0,0 +1,8 @@
+$convert_buildings_to_fuel_flag         ! N/A, flag to convert QUIC-URB buildings to fuel (0 = no, 1 = yes)
+$building_fuel_density                  ! kg/m3, thin fuel density within buildings (if no fuel is specified)
+$building_attenuation_coefficient       ! N/A, attenuation coefficient within buildings
+$building_surface_roughness             ! m, surface roughness within buildings
+$convert_fuel_to_canopy_flag            ! N/A, flag to convert fuel to canopy for winds (0 = no, 1 = yes)
+$update_canopy_winds_flag               ! N/A, update canopy winds when fuel is consumed (0 = no, 1 = yes)
+$fuel_attenuation_coefficient           ! N/A, attenuation coefficient within fuel for the Cionco profile (default = 1)
+$fuel_surface_roughness                 ! m, surface roughness within fuel (default = 0.1 m)
diff --git a/quicfire_tools/inputs/templates/latest/QFire_Plume_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/latest/QFire_Plume_Advanced_User_Inputs.inp
new file mode 100755
index 0000000..e9b36cf
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QFire_Plume_Advanced_User_Inputs.inp
@@ -0,0 +1,16 @@
+$max_plumes_per_timestep                ! N/A, max number of plume at each time step
+$min_plume_updraft_velocity				! m/s, minimum vertical velocity of a plume. If wc is below minimum, the plume is eliminated (default 0.1 m/s)
+$max_plume_updraft_velocity				! m/s, maximum vertical velocity of a plume (default 100 m/s)
+$min_velocity_ratio				        ! N/A, minimum speed ratio (plume vertical velocity/wind speed). If below, the plume is eliminated (default 0.1)
+$brunt_vaisala_freq_squared				! 1/s2, brunt vaisala frequency squared
+$creeping_flag					        ! N/A creeping flag: 0 = off, 1 = on
+$adaptive_timestep_flag					! Flag time step (0 = constant, 1 = adaptive)
+$plume_timestep                         ! s, time step used to compute the buoyant plume trajectory
+$sor_option_flag                        ! sor option (0 = reset lambda at each call, 1 = keep)
+$sor_alpha_plume_center                 ! alpha 2 - plume centerline
+$sor_alpha_plume_edge                   ! alpha 2 - plume edges
+$max_plume_merging_angle                ! deg, max angle between plumes to merging
+$max_plume_overlap_fraction             ! N/A, fraction of overlap of plumes for merging
+$plume_to_grid_updrafts_flag            ! N/A, which wplume-to-grid scheme: 0 = new, 1 = old
+$max_points_along_plume_edge            ! N/A, number of points per edge for new scheme (only new scheme)
+$plume_to_grid_intersection_flag        ! N/A, w computation: 1 = max(w), 0 = sum(w^3)
diff --git a/quicfire_tools/input-templates/v5.2.3/QFire_Pyvista.inp b/quicfire_tools/inputs/templates/latest/QFire_Pyvista.inp
similarity index 99%
rename from quicfire_tools/input-templates/v5.2.3/QFire_Pyvista.inp
rename to quicfire_tools/inputs/templates/latest/QFire_Pyvista.inp
index d2a32d6..af7515e 100644
--- a/quicfire_tools/input-templates/v5.2.3/QFire_Pyvista.inp
+++ b/quicfire_tools/inputs/templates/latest/QFire_Pyvista.inp
@@ -25,4 +25,3 @@ camOrbit=0
 windBarNormalized=none
 windBarBounds=[-10.0,10.0]
 compile=0 run=1 compiler='gfortran' compiler_mode='release' testcase='LineFire' ./adv_compile_and_run.sh
-
diff --git a/quicfire_tools/input-templates/latest/QP_buildout.inp b/quicfire_tools/inputs/templates/latest/QP_buildout.inp
similarity index 100%
rename from quicfire_tools/input-templates/latest/QP_buildout.inp
rename to quicfire_tools/inputs/templates/latest/QP_buildout.inp
diff --git a/quicfire_tools/inputs/templates/latest/QUIC_fire.inp b/quicfire_tools/inputs/templates/latest/QUIC_fire.inp
new file mode 100644
index 0000000..552fa12
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QUIC_fire.inp
@@ -0,0 +1,42 @@
+$fire_flag         ! Fire flag: 1 = run fire; 0 = no fire
+$random_seed               ! Random number generator: -1: use time and date, any other integer > 0 is used as the seed
+! FIRE TIMES
+$time_now		    ! When the fire is ignited in Unix Epoch time (integer seconds since 1970/1/1 00:00:00). Must be greater or equal to the time of the first wind
+$sim_time			! Total simulation time for the fire [s]
+$fire_time_step		! time step for the fire simulation [s]
+$quic_time_step		! Number of fire time steps done before updating the quic wind field (integer, >= 1)
+$out_time_fire      ! After how many fire time steps to print out fire-related files (excluding emissions and radiation)
+$out_time_wind      ! After how many quic updates to print out wind-related files
+$out_time_emis_rad  ! After how many fire time steps to average emissions and radiation
+$out_time_wind_avg  ! After how many quic updates to print out averaged wind-related files
+! FIRE GRID
+$nz					! Number of vertical layers of fire grid cells (integer)
+$stretch_grid_flag  ! Vertical stretching flag: 0 = uniform dz, 1 = custom
+$stretch_grid_input ! m, dz
+! FILE PATH
+""
+1                   ! Fuel types are in separate files
+2                   ! File is stream (1) or headers (2)
+! FUEL
+$fuel_lines
+! IGNITION LOCATIONS
+$ignition_lines
+$ignitions_per_cell ! number of ignitions
+! FIREBRANDS
+$firebrand_flag		! 0 = off, 1 = on
+! OUTPUT FILES (formats depend on the grid type flag)
+$eng_to_atm_out		! Output gridded energy-to-atmosphere (3D fire grid + extra layers)
+$react_rate_out		! Output compressed array reaction rate (fire grid)
+$fuel_dens_out		! Output compressed array fuel density (fire grid)
+$QF_wind_out	    ! Output gridded wind (u,v,w,sigma) (3D fire grid)
+$QU_wind_inst_out	! Output gridded QU winds with fire effects, instantaneous (QUIC-URB grid)
+$QU_wind_avg_out		! Output gridded QU winds with fire effects, averaged (QUIC-URB grid)
+0					! Output plume trajectories (ONLY FOR DEBUG)
+$fuel_moist_out		! Output compressed array fuel moisture (fire grid)
+$mass_burnt_out		! Output vertically-integrated % mass burnt (fire grid)
+$firebrand_out		! Output trajectories firebrands
+$emissions_out		! Output compressed array emissions (fire grid)
+$radiation_out		! Output gridded thermal radiation (fire grid)
+$intensity_out      ! Output surface fire intensity at every fire time step
+! AUTOKILL
+$auto_kill          ! Kill if the fire is out and there are no more ignitions or firebrands (0 = no, 1 = yes)
diff --git a/quicfire_tools/inputs/templates/latest/QU_TopoInputs.inp b/quicfire_tools/inputs/templates/latest/QU_TopoInputs.inp
new file mode 100644
index 0000000..60b3ae4
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QU_TopoInputs.inp
@@ -0,0 +1,11 @@
+Specify file name for custom topo (full path)
+$filename
+$topo_lines
+$smoothing_method   ! N/A, smoothing method (0 = none (default for idealized terrain), 1 = blur, 2 = David's)
+$smoothing_passes   ! N/A, number of smoothing passes
+$sor_iterations     ! N/A, number of initial SOR iterations (only if fire is run)
+$sor_cycles         ! N/A, sor cycles
+$sor_relax          ! N/A, sor relaxation parameter (default for flat is 1.78, ignored if there is no terrain)
+0                   ! N/A, add slope flow correction (0 = no, 1 = yes)
+Command to run slope flow code (system specific)
+""
diff --git a/quicfire_tools/inputs/templates/latest/QU_buildings.inp b/quicfire_tools/inputs/templates/latest/QU_buildings.inp
new file mode 100755
index 0000000..dfc62c4
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QU_buildings.inp
@@ -0,0 +1,4 @@
+!QUIC 6.26
+$wall_roughness_length			!Wall roughness length (m)
+$number_of_buildings			!Number of Buildings
+$number_of_polygon_nodes			!Number of Polygon Building Nodes
diff --git a/quicfire_tools/input-templates/v5.1.1/QU_fileoptions.inp b/quicfire_tools/inputs/templates/latest/QU_fileoptions.inp
old mode 100644
new mode 100755
similarity index 87%
rename from quicfire_tools/input-templates/v5.1.1/QU_fileoptions.inp
rename to quicfire_tools/inputs/templates/latest/QU_fileoptions.inp
index 9eeefa0..605458f
--- a/quicfire_tools/input-templates/v5.1.1/QU_fileoptions.inp
+++ b/quicfire_tools/inputs/templates/latest/QU_fileoptions.inp
@@ -3,4 +3,4 @@
 0   !flag to write out non-mass conserved initial field (uofield.dat) (1=write,0=no write)
 0   !flag to write out the file uosensorfield.dat, the initial sensor velocity field (1=write,0=no write)
 0   !flag to write out the file QU_staggered_velocity.bin used by QUIC-Pressure(1=write,0=no write)
-0   !flag to generate startup files (0 = no, 1 = yes)
\ No newline at end of file
+0   !flag to generate startup files (0 = no, 1 = yes)
diff --git a/quicfire_tools/input-templates/latest/QU_landuse.inp b/quicfire_tools/inputs/templates/latest/QU_landuse.inp
similarity index 100%
rename from quicfire_tools/input-templates/latest/QU_landuse.inp
rename to quicfire_tools/inputs/templates/latest/QU_landuse.inp
diff --git a/quicfire_tools/input-templates/latest/QU_metparams.inp b/quicfire_tools/inputs/templates/latest/QU_metparams.inp
similarity index 58%
rename from quicfire_tools/input-templates/latest/QU_metparams.inp
rename to quicfire_tools/inputs/templates/latest/QU_metparams.inp
index a41fb25..3f801e8 100644
--- a/quicfire_tools/input-templates/latest/QU_metparams.inp
+++ b/quicfire_tools/inputs/templates/latest/QU_metparams.inp
@@ -1,7 +1,5 @@
 !QUIC 6.26
 0 !Met input flag (0=QUIC,1=WRF,2=ITT MM5,3=HOTMAC)
-1 !Number of measuring sites
+$num_sensors    !Number of measuring sites
 1 !Maximum size of data points profiles
-sensor1 !Site Name 
-!File name
-sensor1.inp
+$sensor_lines
diff --git a/quicfire_tools/input-templates/latest/QU_movingcoords.inp b/quicfire_tools/inputs/templates/latest/QU_movingcoords.inp
similarity index 100%
rename from quicfire_tools/input-templates/latest/QU_movingcoords.inp
rename to quicfire_tools/inputs/templates/latest/QU_movingcoords.inp
diff --git a/quicfire_tools/inputs/templates/latest/QU_simparams.inp b/quicfire_tools/inputs/templates/latest/QU_simparams.inp
new file mode 100755
index 0000000..d1adf95
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/QU_simparams.inp
@@ -0,0 +1,30 @@
+!QUIC 6.26
+$nx ! nx - Domain Length (X) Grid Cells
+$ny ! ny - Domain Width (Y) Grid Cells
+$nz ! nz - domain height (Z) grid cells
+$dx ! DX [m]
+$dy ! DY [m]
+$stretch_grid_flag ! Vertical stretching flag (0 = uniform, 1 = custom, 2 = parabolic Z, 3 = parabolic DZ, 4 = exponential)
+$vertical_grid_lines
+$wind_time_lines
+2 ! (NOT USED) Rooftop flag (0 = none, 1 = log profile, 2 = vortex)
+3 ! (NOT USED) Upwind cavity flag (0 = none, 1 = Rockle, 2 = MVP, 3 = HMVP)
+4 ! (NOT USED) Street canyon flag (0 = none, 1 = Roeckle, 2 = CPB, 3 = exp. param. PKK, 4 = Roeckle w/ Fackrel)
+1 ! (NOT USED) Street intersection flag (0 = off, 1 = on)
+4 ! (NOT USED) Wake flag (0 = none, 1 = Rockle, 2 = modified Rockle, 3 = area scaled)
+1 ! (NOT USED) Sidewall flag (0 = off, 1 = on)
+1 ! (NOT USED) Individual tree wake flag
+2 ! (NOT USED) Canopy flag (0 = off, 1 = Cionco w/o wakes, 2 = Cionco w/ wakes)
+1 ! (NOT USED) Season flag (1 = Summer, 2 = Winter, 3 = Transition)
+$sor_iter_max ! Maximum number of iterations
+$sor_residual_reduction ! Residual reduction (Orders of Magnitude)
+$use_diffusion_flag ! Use diffusion algorithm (0 = off, 1 = on)
+$number_diffusion_iterations ! Number of diffusion iterations
+$domain_rotation ! Domain rotation relative to true north [degrees] (cw = +)
+$utm_x ! UTMX of domain origin [m]
+$utm_y ! UTMY of domain origin [m]
+$utm_zone_number ! UTM zone
+$utm_zone_letter ! UTM zone leter (1 = A, 2 = B, etc.)
+$quic_cfd_flag ! QUIC-CFD Flag
+$explosive_bldg_flag ! Explosive building damage flag (1 = on)
+$bldg_array_flag ! Building Array Flag (1 = on)
diff --git a/quicfire_tools/input-templates/latest/Runtime_Advanced_User_Inputs.inp b/quicfire_tools/inputs/templates/latest/Runtime_Advanced_User_Inputs.inp
similarity index 100%
rename from quicfire_tools/input-templates/latest/Runtime_Advanced_User_Inputs.inp
rename to quicfire_tools/inputs/templates/latest/Runtime_Advanced_User_Inputs.inp
diff --git a/quicfire_tools/inputs/templates/latest/gridlist b/quicfire_tools/inputs/templates/latest/gridlist
new file mode 100644
index 0000000..34df7c1
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/gridlist
@@ -0,0 +1,7 @@
+n=$n
+m=$m
+l=$l
+aa1=$aa1
+dx=$dx
+dy=$dy
+dz=$dz
diff --git a/quicfire_tools/input-templates/v5.2.3/rasterorigin.txt b/quicfire_tools/inputs/templates/latest/rasterorigin.txt
similarity index 77%
rename from quicfire_tools/input-templates/v5.2.3/rasterorigin.txt
rename to quicfire_tools/inputs/templates/latest/rasterorigin.txt
index b0916ba..0cd1bf6 100644
--- a/quicfire_tools/input-templates/v5.2.3/rasterorigin.txt
+++ b/quicfire_tools/inputs/templates/latest/rasterorigin.txt
@@ -1,7 +1,7 @@
-0.
-0.
+$utm_x
+$utm_y
 752265.868913356
 3752846.04249607
 742265.868913356
 3742846.04249607
-10000
\ No newline at end of file
+10000
diff --git a/quicfire_tools/inputs/templates/latest/sensor1.inp b/quicfire_tools/inputs/templates/latest/sensor1.inp
new file mode 100644
index 0000000..82c0949
--- /dev/null
+++ b/quicfire_tools/inputs/templates/latest/sensor1.inp
@@ -0,0 +1,7 @@
+sensor1 !Site Name
+0 !Upper level flag (1 = use this profile for upper level winds)
+50 !Upper level height (meters)
+1 !Site Coordinate Flag (1=QUIC, 2=UTM, 3=Lat/Lon)
+1 !X coordinate (meters)
+1 !Y coordinate (meters)
+$wind_lines
diff --git a/requirements/tests-310.yml b/requirements/tests-310.yml
index fdbebf1..224245b 100644
--- a/requirements/tests-310.yml
+++ b/requirements/tests-310.yml
@@ -13,4 +13,4 @@ dependencies:
   - zarr
   - numpy
   - scipy
-  - dask
\ No newline at end of file
+  - dask
diff --git a/requirements/tests-311.yml b/requirements/tests-311.yml
index c126e27..b3fe58a 100644
--- a/requirements/tests-311.yml
+++ b/requirements/tests-311.yml
@@ -13,4 +13,4 @@ dependencies:
   - zarr
   - numpy
   - scipy
-  - dask
\ No newline at end of file
+  - dask
diff --git a/requirements/tests-38.yml b/requirements/tests-38.yml
index a45ee9f..30f8b5c 100644
--- a/requirements/tests-38.yml
+++ b/requirements/tests-38.yml
@@ -13,4 +13,4 @@ dependencies:
   - zarr
   - numpy
   - scipy
-  - dask
\ No newline at end of file
+  - dask
diff --git a/requirements/tests-39.yml b/requirements/tests-39.yml
index dd3c67d..237907f 100644
--- a/requirements/tests-39.yml
+++ b/requirements/tests-39.yml
@@ -13,4 +13,4 @@ dependencies:
   - zarr
   - numpy
   - scipy
-  - dask
\ No newline at end of file
+  - dask
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..7dcdbfc
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,8 @@
+import os
+
+TEST_ENV = os.environ.get("TEST_ENV", "local")
+
+if TEST_ENV == "local":
+    import sys
+
+    sys.path.append("../quicfire_tools")
diff --git a/tests/data/test-templates/stretchgrid_0.txt b/tests/data/test-templates/stretchgrid_0.txt
new file mode 100644
index 0000000..e7aeccc
--- /dev/null
+++ b/tests/data/test-templates/stretchgrid_0.txt
@@ -0,0 +1,2 @@
+1.0	! Surface DZ [m]
+5	! Number of uniform surface cells
\ No newline at end of file
diff --git a/tests/data/test-templates/stretchgrid_1.txt b/tests/data/test-templates/stretchgrid_1.txt
new file mode 100644
index 0000000..fb1760e
--- /dev/null
+++ b/tests/data/test-templates/stretchgrid_1.txt
@@ -0,0 +1,28 @@
+1.0	! Surface DZ [m]
+! DZ array [m]
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
\ No newline at end of file
diff --git a/tests/data/test-templates/stretchgrid_3.txt b/tests/data/test-templates/stretchgrid_3.txt
new file mode 100644
index 0000000..b3f8e15
--- /dev/null
+++ b/tests/data/test-templates/stretchgrid_3.txt
@@ -0,0 +1,29 @@
+1.0	! Surface DZ [m]
+5	! Number of uniform surface cells
+! DZ array [m]
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0
+1.0954701278755725
+1.38188051150229
+1.8592311508801522
+2.5275220460091594
+3.3867531968893108
+4.436924603520609
+5.678036265903049
+7.110088184036636
+8.733080357921368
+10.547012787557245
+12.551885472944265
+14.74769841408243
+17.134451610971745
+19.712145063612198
+22.480778772003795
+25.440352736146544
+28.590866956040436
+31.932321431685473
+35.46471616308165
+39.18878414733607
\ No newline at end of file
diff --git a/tests/data/test-templates/wind_times.txt b/tests/data/test-templates/wind_times.txt
new file mode 100644
index 0000000..4e23d79
--- /dev/null
+++ b/tests/data/test-templates/wind_times.txt
@@ -0,0 +1,4 @@
+1	! Number of time increments
+0	! UTC offset [hours]
+! Wind step times [s]
+0
\ No newline at end of file
diff --git a/tests/test_input_module.py b/tests/test_input_module.py
index e0446f3..2e3e5fa 100644
--- a/tests/test_input_module.py
+++ b/tests/test_input_module.py
@@ -1,340 +1,1711 @@
-# Core Imports
-import sys
+"""
+Test module for the inputs module of the quicfire_tools package.
+"""
+import pytest
+from pydantic import ValidationError
+from pathlib import Path
 
-# Internal Imports
-sys.path.append("../quicfire_tools")
-from quicfire_tools.inputs import SimulationInputs
+from quicfire_tools.inputs import (
+    Gridlist,
+    RasterOrigin,
+    QU_Buildings,
+    QU_Fileoptions,
+    QU_Simparams,
+    QFire_Advanced_User_Inputs,
+    QUIC_fire,
+    QFire_Bldg_Advanced_User_Inputs,
+    QFire_Plume_Advanced_User_Inputs,
+    QU_TopoInputs,
+    RuntimeAdvancedUserInputs,
+    QU_movingcoords,
+    QP_buildout,
+    QU_metparams,
+    Sensor1,
+    SimulationInputs,
+)
+from quicfire_tools.ignitions import RectangleIgnition
+from quicfire_tools.topography import TopoType, GaussianHillTopo
 
-# External Imports
-import pytest
+# Create a tmp/ directory to store the temporary test files
+Path("tmp/").mkdir(exist_ok=True)
+
+
+class TestGridList:
+    def test_init(self):
+        """Test the initialization of a Gridlist object."""
+        # Test the default initialization
+        gridlist = Gridlist(n=10, m=10, l=10, dx=1, dy=1, dz=1, aa1=1)
+        assert isinstance(gridlist, Gridlist)
+        assert gridlist.n == 10
+        for i in ["n", "m", "l", "dx", "dy", "dz", "aa1"]:
+            assert i in gridlist.list_parameters()
+
+        # Test data type casting
+        gridlist = Gridlist(n="10", m=10, l=10, dx=1, dy=1, dz=1, aa1=1)
+        assert isinstance(gridlist.n, int)
+        assert gridlist.n == 10
+
+        # Pass bad parameters: non-real numbers
+        with pytest.raises(ValidationError):
+            Gridlist(n=2.5, m=10, l=10, dx="", dy=1, dz=1, aa1=1)
+        with pytest.raises(ValidationError):
+            Gridlist(n="a", m=10, l=10, dx=1, dy=1, dz=1, aa1=1)
+
+        # Pass bad parameters: zero or negative values
+        with pytest.raises(ValidationError):
+            Gridlist(n=-10, m=10, l=10, dx=0, dy=1, dz=1, aa1=1)
+        with pytest.raises(ValidationError):
+            Gridlist(n=10, m=10, l=10, dx=1, dy=0, dz=1, aa1=1)
+
+    def test_to_dict(self):
+        gridlist = Gridlist(n=10, m=10, l=10, dx=1, dy=1, dz=1, aa1=1)
+        result_dict = gridlist.to_dict()
+        assert result_dict["n"] == 10
+        assert result_dict["m"] == 10
+        assert result_dict["l"] == 10
+        assert result_dict["dx"] == 1
+        assert result_dict["dy"] == 1
+        assert result_dict["dz"] == 1
+        assert "_validate_inputs" not in result_dict
+
+    def test_to_docs(self):
+        gridlist = Gridlist(n=10, m=10, l=10, dx=1, dy=1, dz=1, aa1=1)
+        result_dict = gridlist.to_dict()
+        result_docs = gridlist.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        """Test the write_file method of a Gridlist object."""
+        gridlist = Gridlist(n=10, m=10, l=10, dx=1.0, dy=1.0, dz=1.0, aa1=1.0)
+        gridlist.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/gridlist", "r") as file:
+            lines = file.readlines()
+            assert lines[0].split("=")[1].strip() == "10"
+            assert lines[1].split("=")[1].strip() == "10"
+            assert lines[2].split("=")[1].strip() == "10"
+            assert lines[3].split("=")[1].strip() == "1.0"
+            assert lines[4].split("=")[1].strip() == "1.0"
+            assert lines[5].split("=")[1].strip() == "1.0"
+            assert lines[6].split("=")[1].strip() == "1.0"
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            gridlist.to_file("/non_existent_path/gridlist.txt")
+
+
+class TestRasterOrigin:
+    def test_init(self):
+        """Test the initialization of a RasterOrigin object."""
+        # Test the default initialization
+        raster_origin = RasterOrigin()
+        assert isinstance(raster_origin, RasterOrigin)
+        assert raster_origin.utm_x == 0.0
+        assert raster_origin.utm_y == 0.0
+
+        # Test the default initialization
+        raster_origin = RasterOrigin(utm_x=500.0, utm_y=1000.0)
+        assert isinstance(raster_origin, RasterOrigin)
+        assert raster_origin.utm_x == 500.0
+        assert raster_origin.utm_y == 1000.0
+
+        # Test data type casting
+        raster_origin = RasterOrigin(utm_x="500", utm_y=1000.0)
+        assert isinstance(raster_origin.utm_x, float)
+        assert raster_origin.utm_x == 500.0
+
+        # Pass bad parameters: non-real numbers
+        with pytest.raises(ValidationError):
+            RasterOrigin(utm_x="x", utm_y=1000.0)
+
+        # Pass bad parameters: zero or negative values
+        with pytest.raises(ValidationError):
+            RasterOrigin(utm_x=-1, utm_y=1000.0)
+        with pytest.raises(ValidationError):
+            RasterOrigin(utm_x=500.0, utm_y=-1000.0)
+
+    def test_to_dict(self):
+        """Test the to_dict method of a RasterOrigin object."""
+        raster_origin = RasterOrigin(utm_x=500.0, utm_y=1000.0)
+        result_dict = raster_origin.to_dict()
+        assert result_dict["utm_x"] == raster_origin.utm_x
+        assert result_dict["utm_y"] == raster_origin.utm_y
+
+    def test_from_dict(self):
+        """Test the from_dict method of a RasterOrigin object."""
+        raster_origin = RasterOrigin(utm_x=500.0, utm_y=1000.0)
+        result_dict = raster_origin.to_dict()
+        test_object = RasterOrigin.from_dict(result_dict)
+        assert isinstance(test_object, RasterOrigin)
+        assert raster_origin == test_object
+
+    def test_to_docs(self):
+        raster_origin = RasterOrigin(utm_x=500.0, utm_y=1000.0)
+        result_dict = raster_origin.to_dict()
+        result_docs = raster_origin.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        """Test the to_file method of a RasterOrigin object."""
+        raster_origin = RasterOrigin(utm_x=500.0, utm_y=1000.0)
+        raster_origin.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/rasterorigin.txt", "r") as file:
+            lines = file.readlines()
+            assert float(lines[0].strip()) == raster_origin.utm_x
+            assert float(lines[1].strip()) == raster_origin.utm_y
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            raster_origin.to_file("/non_existent_path/rasterorigin.txt")
+
+    def test_from_file(self):
+        """Test initializing a class from a rasterorigin.txt file."""
+        raster_origin = RasterOrigin()
+        raster_origin.to_file("tmp/")
+        test_object = RasterOrigin.from_file("tmp/")
+        assert isinstance(test_object, RasterOrigin)
+        assert raster_origin == test_object
+
+
+class TestQU_Buildings:
+    def test_init(self):
+        """Test the initialization of a QU_Buildings object."""
+        # Test the default initialization
+        qu_buildings = QU_Buildings()
+        assert qu_buildings.wall_roughness_length == 0.1
+        assert qu_buildings.number_of_buildings == 0
+        assert qu_buildings.number_of_polygon_nodes == 0
+
+        # Test custom initialization
+        qu_buildings = QU_Buildings(
+            wall_roughness_length=1.0, number_of_buildings=0, number_of_polygon_nodes=0
+        )
+        assert qu_buildings.wall_roughness_length == 1.0
+        assert qu_buildings.number_of_buildings == 0
+        assert qu_buildings.number_of_polygon_nodes == 0
+
+        # Test data type casting
+        qu_buildings = QU_Buildings(
+            wall_roughness_length="1.0", number_of_buildings=1.0
+        )
+        assert isinstance(qu_buildings.wall_roughness_length, float)
+        assert qu_buildings.wall_roughness_length == 1.0
+        assert isinstance(qu_buildings.number_of_buildings, int)
+        assert qu_buildings.number_of_buildings == 1
+
+        # Pass bad parameters
+        with pytest.raises(ValidationError):
+            QU_Buildings(
+                wall_roughness_length=-1,
+                number_of_buildings=0,
+                number_of_polygon_nodes=0,
+            )
+        with pytest.raises(ValidationError):
+            QU_Buildings(
+                wall_roughness_length=1,
+                number_of_buildings=-1,
+                number_of_polygon_nodes=0,
+            )
+        with pytest.raises(ValidationError):
+            QU_Buildings(wall_roughness_length=0)
+
+    def test_to_dict(self):
+        """Test the to_dict method of a QU_Buildings object."""
+        qu_buildings = QU_Buildings()
+        result_dict = qu_buildings.to_dict()
+        assert (
+            result_dict["wall_roughness_length"] == qu_buildings.wall_roughness_length
+        )
+        assert result_dict["number_of_buildings"] == qu_buildings.number_of_buildings
+        assert (
+            result_dict["number_of_polygon_nodes"]
+            == qu_buildings.number_of_polygon_nodes
+        )
+
+    def test_from_dict(self):
+        """Test the from_dict method of a QU_Buildings object."""
+        qu_buildings = QU_Buildings()
+        result_dict = qu_buildings.to_dict()
+        test_object = QU_Buildings.from_dict(result_dict)
+        assert isinstance(test_object, QU_Buildings)
+        assert qu_buildings == test_object
+
+    def test_to_docs(self):
+        qu_buildings = QU_Buildings()
+        result_dict = qu_buildings.to_dict()
+        result_docs = qu_buildings.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        """Test the to_file method of a QU_Buildings object."""
+        qu_buildings = QU_Buildings()
+        qu_buildings.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QU_buildings.inp", "r") as file:
+            lines = file.readlines()
+            assert (
+                float(lines[1].strip().split("\t")[0])
+                == qu_buildings.wall_roughness_length
+            )
+            assert (
+                int(lines[2].strip().split("\t")[0]) == qu_buildings.number_of_buildings
+            )
+            assert (
+                int(lines[3].strip().split("\t")[0])
+                == qu_buildings.number_of_polygon_nodes
+            )
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            qu_buildings.to_file("/non_existent_path/QU_buildings.inp")
+
+    def test_from_file(self):
+        """Test initializing a class from a QU_buildings.inp file."""
+        qu_buildings = QU_Buildings()
+        qu_buildings.to_file("tmp/")
+        test_object = QU_Buildings.from_file("tmp/")
+        assert isinstance(test_object, QU_Buildings)
+        assert qu_buildings == test_object
+
+
+class TestQU_Fileoptions:
+    def test_init(self):
+        # Test default initialization
+        qu_fileoptions = QU_Fileoptions()
+        assert qu_fileoptions.output_data_file_format_flag == 2
+        assert qu_fileoptions.non_mass_conserved_initial_field_flag == 0
+        assert qu_fileoptions.initial_sensor_velocity_field_flag == 0
+        assert qu_fileoptions.qu_staggered_velocity_file_flag == 0
+        assert qu_fileoptions.generate_wind_startup_files_flag == 0
+
+        # Test custom initialization #1
+        qu_fileoptions = QU_Fileoptions(output_data_file_format_flag=1)
+        assert qu_fileoptions.output_data_file_format_flag == 1
+
+        # Test custom initialization #2
+        qu_fileoptions = QU_Fileoptions(non_mass_conserved_initial_field_flag=1)
+        assert qu_fileoptions.non_mass_conserved_initial_field_flag == 1
+
+        # Test custom initialization #3
+        qu_fileoptions = QU_Fileoptions(generate_wind_startup_files_flag=1)
+        assert qu_fileoptions.generate_wind_startup_files_flag == 1
+
+        # Test invalid output_data_file_format_flag flags
+        for invalid_flag in [-1, 0, 5, "1", 1.0, 1.5]:
+            with pytest.raises(ValidationError):
+                QU_Fileoptions(output_data_file_format_flag=invalid_flag)
+
+        # Test invalid non_mass_conserved_initial_field_flag flag
+        for invalid_flag in [-1, 0.0, "1", 2]:
+            with pytest.raises(ValidationError):
+                QU_Fileoptions(non_mass_conserved_initial_field_flag=invalid_flag)
+
+    def test_to_dict(self):
+        """Test the to_dict method of a QU_Buildings object."""
+        qu_fileoptions = QU_Fileoptions()
+        result_dict = qu_fileoptions.to_dict()
+        assert (
+            result_dict["output_data_file_format_flag"]
+            == qu_fileoptions.output_data_file_format_flag
+        )
+        assert (
+            result_dict["non_mass_conserved_initial_field_flag"]
+            == qu_fileoptions.non_mass_conserved_initial_field_flag
+        )
+        assert (
+            result_dict["initial_sensor_velocity_field_flag"]
+            == qu_fileoptions.initial_sensor_velocity_field_flag
+        )
+        assert (
+            result_dict["qu_staggered_velocity_file_flag"]
+            == qu_fileoptions.qu_staggered_velocity_file_flag
+        )
+        assert (
+            result_dict["generate_wind_startup_files_flag"]
+            == qu_fileoptions.generate_wind_startup_files_flag
+        )
+
+    def test_from_dict(self):
+        """Test the from_dict method of a QU_Buildings object."""
+        qu_fileoptions = QU_Fileoptions()
+        result_dict = qu_fileoptions.to_dict()
+        test_object = QU_Fileoptions.from_dict(result_dict)
+        assert isinstance(test_object, QU_Fileoptions)
+        assert qu_fileoptions == test_object
+
+    def test_to_docs(self):
+        qu_fileoptions = QU_Fileoptions()
+        result_dict = qu_fileoptions.to_dict()
+        result_docs = qu_fileoptions.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+
+    def test_to_file(self):
+        """Test the to_file method of a QU_Buildings object."""
+        qu_fileoptions = QU_Fileoptions()
+        qu_fileoptions.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QU_fileoptions.inp", "r") as file:
+            lines = file.readlines()
+            assert int(lines[1].strip().split("!")[0]) == 2
+            assert int(lines[2].strip().split("!")[0]) == 0
+            assert int(lines[3].strip().split("!")[0]) == 0
+            assert int(lines[4].strip().split("!")[0]) == 0
+            assert int(lines[5].strip().split("!")[0]) == 0
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            qu_fileoptions.to_file("/non_existent_path/QU_buildings.inp")
+
+    def test_from_file(self):
+        """Test initializing a class from a QU_fileoptions.inp file."""
+        qu_fileoptions = QU_Fileoptions()
+        qu_fileoptions.to_file("tmp/")
+        test_object = QU_Fileoptions.from_file("tmp/")
+        assert isinstance(test_object, QU_Fileoptions)
+        assert qu_fileoptions == test_object
+
+
+class TestQU_Simparams:
+    @staticmethod
+    def get_test_object():
+        return QU_Simparams(nx=100, ny=100, nz=26, dx=2.0, dy=2, quic_domain_height=250)
+
+    def test_init(self):
+        # Test default initialization
+        qu_simparams = self.get_test_object()
+        assert qu_simparams.nx == 100
+        assert qu_simparams.ny == 100
+        assert qu_simparams.nz == 26
+        assert qu_simparams.dx == 2
+        assert qu_simparams.dy == 2
+        assert qu_simparams.surface_vertical_cell_size == 1
+        assert qu_simparams.number_surface_cells == 5
+        assert qu_simparams.stretch_grid_flag == 3
+        assert len(qu_simparams.dz_array) == 26
+        assert len(qu_simparams.vertical_grid_lines.split("\n")) == 29
+
+        # Test changing the default values
+        qu_simparams.nx = 150
+        assert qu_simparams.nx == 150
+
+        # Test property setters
+        qu_simparams.nz = 30
+        assert qu_simparams.nz == 30
+        assert len(qu_simparams.dz_array) == 30
+        assert len(qu_simparams.vertical_grid_lines.split("\n")) == 33
+
+        # Test data type casting
+        qu_simparams = QU_Simparams(
+            nx="100", ny=100, nz=26, dx=2, dy=2, quic_domain_height=5
+        )
+        assert isinstance(qu_simparams.nx, int)
+        assert qu_simparams.nx == 100
+
+        # Test with custom dz_array
+        # TODO: Come back to this tests
+        # qu_simparams = QU_Simparams(nx=100, ny=100, nz=26, dx=2, dy=2,
+        #                             custom_dz_array=[1] * 26,
+        #                             quic_domain_height=250)
+        # assert qu_simparams.dz_array == [
+        #     qu_simparams.surface_vertical_cell_size] * 26
+
+        # Test invalid stretch_grid_flags
+        for invalid_flag in [-1, 4, "1", 1.0, 1.5, 2]:
+            with pytest.raises(ValidationError):
+                QU_Simparams(stretch_grid_flag=invalid_flag)
+
+    def test_dz_array(self):
+        # Test with stretch_grid_flag = 0
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 0
+        assert (
+            qu_simparams.dz_array
+            == [qu_simparams.surface_vertical_cell_size] * qu_simparams.nz
+        )
+
+        # Test with stretch_grid_flag = 1
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 1
+        qu_simparams.custom_dz_array = [0.5] * qu_simparams.nz
+        assert qu_simparams.dz_array == [0.5] * qu_simparams.nz
+
+        # Test with stretch_grid_flag = 3
+        qu_simparams = self.get_test_object()
+        assert len(qu_simparams.dz_array) == qu_simparams.nz
+
+    def test_stretch_grid_flag_0(self):
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 0
+        vertical_grid_lines = qu_simparams._stretch_grid_flag_0()
+        with open("data/test-templates/stretchgrid_0.txt") as f:
+            expected_lines = f.readlines()
+        assert vertical_grid_lines == "".join(expected_lines)
+
+    def test_stretch_grid_flag_1(self):
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 1
+
+        # Test with no dz_array input
+        with pytest.raises(ValueError):
+            qu_simparams._stretch_grid_flag_1()
+
+        # Test with 19 custom_dz_array inputs
+        qu_simparams.custom_dz_array = [1] * (qu_simparams.nz - 1)
+        with pytest.raises(ValueError):
+            qu_simparams._stretch_grid_flag_1()
+
+        # Test with dz inputs that don't match the surface values
+        qu_simparams.custom_dz_array = [1] * qu_simparams.nz
+        qu_simparams.custom_dz_array[0] = 2
+        with pytest.raises(ValueError):
+            qu_simparams._stretch_grid_flag_1()
+
+        # Test valid case
+        qu_simparams.custom_dz_array = [1] * qu_simparams.nz
+        vertical_grid_lines = qu_simparams._stretch_grid_flag_1()
+        with open("data/test-templates/stretchgrid_1.txt") as f:
+            expected_lines = f.readlines()
+        assert vertical_grid_lines == "".join(expected_lines)
+
+    def test_stretch_grid_flag_3(self):
+        qu_simparams = self.get_test_object()
+        vertical_grid_lines = qu_simparams._stretch_grid_flag_3()
+        with open("data/test-templates/stretchgrid_3.txt") as f:
+            expected_lines = f.readlines()
+        assert vertical_grid_lines == "".join(expected_lines)
+
+    def test_generate_vertical_grid(self):
+        qu_simparams = self.get_test_object()
+
+        # Test stretch_grid_flag = 0
+        qu_simparams.stretch_grid_flag = 0
+        with open("data/test-templates/stretchgrid_0.txt") as f:
+            expected_lines = f.readlines()
+        assert qu_simparams.vertical_grid_lines == "".join(expected_lines)
+
+        # Test stretch_grid_flag = 1
+        qu_simparams.stretch_grid_flag = 1
+        qu_simparams.custom_dz_array = [1] * qu_simparams.nz
+        with open("data/test-templates/stretchgrid_1.txt") as f:
+            expected_lines = f.readlines()
+        assert qu_simparams.vertical_grid_lines == "".join(expected_lines)
+
+        # Test stretch_grid_flag = 3
+        qu_simparams.stretch_grid_flag = 3
+        with open("data/test-templates/stretchgrid_3.txt") as f:
+            expected_lines = f.readlines()
+        assert qu_simparams.vertical_grid_lines == "".join(expected_lines)
+
+    def test_generate_wind_times(self):
+        # Test valid wind_step_times
+        qu_simparams = self.get_test_object()
+        qu_simparams.wind_times = [0]
+        wind_times_lines = qu_simparams._generate_wind_time_lines()
+        with open("data/test-templates/wind_times.txt") as f:
+            expected_lines = f.readlines()
+        assert wind_times_lines == "".join(expected_lines)
+
+        # Test invalid wind_step_times
+        qu_simparams.wind_times = []
+        with pytest.raises(ValueError):
+            qu_simparams._generate_wind_time_lines()
+
+    def test_to_dict(self):
+        """
+        Test the to_dict method of a QU_Simparams object.
+        """
+        qu_simparams = self.get_test_object()
+        result_dict = qu_simparams.to_dict()
+
+        # Test the passed parameters
+        assert result_dict["nx"] == qu_simparams.nx
+        assert result_dict["ny"] == qu_simparams.ny
+        assert result_dict["nz"] == qu_simparams.nz
+        assert result_dict["dx"] == qu_simparams.dx
+        assert result_dict["dy"] == qu_simparams.dy
+        assert (
+            result_dict["surface_vertical_cell_size"]
+            == qu_simparams.surface_vertical_cell_size
+        )
+        assert result_dict["number_surface_cells"] == qu_simparams.number_surface_cells
+
+        # Test the default parameters
+        assert (
+            result_dict["surface_vertical_cell_size"]
+            == qu_simparams.surface_vertical_cell_size
+        )
+        assert result_dict["number_surface_cells"] == qu_simparams.number_surface_cells
+        assert result_dict["stretch_grid_flag"] == qu_simparams.stretch_grid_flag
+        assert result_dict["dz_array"] == qu_simparams.dz_array
+        assert result_dict["utc_offset"] == qu_simparams.utc_offset
+        assert result_dict["wind_times"] == qu_simparams.wind_times
+        assert result_dict["sor_iter_max"] == qu_simparams.sor_iter_max
+        assert (
+            result_dict["sor_residual_reduction"] == qu_simparams.sor_residual_reduction
+        )
+        assert result_dict["use_diffusion_flag"] == qu_simparams.use_diffusion_flag
+        assert (
+            result_dict["number_diffusion_iterations"]
+            == qu_simparams.number_diffusion_iterations
+        )
+        assert result_dict["domain_rotation"] == qu_simparams.domain_rotation
+        assert result_dict["utm_x"] == qu_simparams.utm_x
+        assert result_dict["utm_y"] == qu_simparams.utm_y
+        assert result_dict["utm_zone_number"] == qu_simparams.utm_zone_number
+        assert result_dict["utm_zone_letter"] == qu_simparams.utm_zone_letter
+        assert result_dict["quic_cfd_flag"] == qu_simparams.quic_cfd_flag
+        assert result_dict["explosive_bldg_flag"] == qu_simparams.explosive_bldg_flag
+        assert result_dict["bldg_array_flag"] == qu_simparams.bldg_array_flag
+
+    def test_from_dict(self):
+        """
+        Test the from_dict method of a QU_Simparams object.
+        """
+        qu_simparams = self.get_test_object()
+        result_dict = qu_simparams.to_dict()
+        test_object = QU_Simparams.from_dict(result_dict)
+        assert isinstance(test_object, QU_Simparams)
+        assert qu_simparams == test_object
+
+    def test_to_docs(self):
+        qu_simparams = self.get_test_object()
+        result_dict = qu_simparams.to_dict()
+        result_docs = qu_simparams.get_documentation()
+        for key in result_dict:
+            if key in ["vertical_grid_lines", "wind_time_lines", "custom_dz_array"]:
+                continue
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        """
+        Test the to_file method of a QU_Simparams object.
+        """
+        qu_simparams = self.get_test_object()
+        qu_simparams.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QU_simparams.inp", "r") as file:
+            lines = file.readlines()
+
+        # Check nx, ny, nz, dx, dy
+        assert int(lines[1].strip().split("!")[0]) == qu_simparams.nx
+        assert int(lines[2].strip().split("!")[0]) == qu_simparams.ny
+        assert int(lines[3].strip().split("!")[0]) == qu_simparams.nz
+        assert float(lines[4].strip().split("!")[0]) == qu_simparams.dx
+        assert float(lines[5].strip().split("!")[0]) == qu_simparams.dy
+
+        # Check stretch_grid_flag, surface_vertical_cell_size,
+        # number_surface_cells
+        assert int(lines[6].strip().split("!")[0]) == qu_simparams.stretch_grid_flag
+        assert (
+            float(lines[7].strip().split("!")[0])
+            == qu_simparams.surface_vertical_cell_size
+        )
+        assert int(lines[8].strip().split("!")[0]) == qu_simparams.number_surface_cells
+
+        # Check dz_array
+        assert lines[9] == "! DZ array [m]\n"
+        for i in range(qu_simparams.nz):
+            index = i + 10
+            dz = qu_simparams.dz_array[i]
+            assert float(lines[index].strip()) == dz
+
+        # Update lines index
+        i_current = 10 + qu_simparams.nz
+
+        # Check number of time increments, utc_offset
+        assert int(lines[i_current].strip().split("!")[0]) == len(
+            qu_simparams.wind_times
+        )
+        assert (
+            int(lines[i_current + 1].strip().split("!")[0]) == qu_simparams.utc_offset
+        )
+
+        # Check wind_step_times
+        assert lines[i_current + 2] == "! Wind step times [s]\n"
+        for i in range(len(qu_simparams.wind_times)):
+            index = i_current + 3 + i
+            wind_time = qu_simparams.wind_times[i]
+            assert int(lines[index].strip()) == wind_time
+
+        # Update lines index
+        i_current = i_current + 3 + len(qu_simparams.wind_times)
+        i_current += 9  # Skip not used lines
+
+        # Check sor_iter_max, sor_residual_reduction
+        assert int(lines[i_current].strip().split("!")[0]) == qu_simparams.sor_iter_max
+        assert (
+            int(lines[i_current + 1].strip().split("!")[0])
+            == qu_simparams.sor_residual_reduction
+        )
+
+        # Check use_diffusion_flag, number_diffusion_iterations, domain_rotation
+        # utm_x, utm_y, utm_zone_number, utm_zone_letter, quic_cfd_flag,
+        # explosive_bldg_flag, bldg_array_flag
+        assert (
+            int(lines[i_current + 2].strip().split("!")[0])
+            == qu_simparams.use_diffusion_flag
+        )
+        assert (
+            int(lines[i_current + 3].strip().split("!")[0])
+            == qu_simparams.number_diffusion_iterations
+        )
+        assert (
+            float(lines[i_current + 4].strip().split("!")[0])
+            == qu_simparams.domain_rotation
+        )
+        assert float(lines[i_current + 5].strip().split("!")[0]) == qu_simparams.utm_x
+        assert float(lines[i_current + 6].strip().split("!")[0]) == qu_simparams.utm_y
+        assert (
+            int(lines[i_current + 7].strip().split("!")[0])
+            == qu_simparams.utm_zone_number
+        )
+        assert (
+            int(lines[i_current + 8].strip().split("!")[0])
+            == qu_simparams.utm_zone_letter
+        )
+        assert (
+            int(lines[i_current + 9].strip().split("!")[0])
+            == qu_simparams.quic_cfd_flag
+        )
+        assert (
+            int(lines[i_current + 10].strip().split("!")[0])
+            == qu_simparams.explosive_bldg_flag
+        )
+        assert (
+            int(lines[i_current + 11].strip().split("!")[0])
+            == qu_simparams.bldg_array_flag
+        )
+
+    def test_from_file(self):
+        """
+        Test initializing a class from a QU_simparams.inp file.
+        """
+        # Test stretch grid flag = 3
+        qu_simparams = self.get_test_object()
+        qu_simparams.to_file("tmp/")
+        test_object = QU_Simparams.from_file("tmp/")
+        assert isinstance(test_object, QU_Simparams)
+        assert qu_simparams == test_object
+
+        # Test stretch grid flag = 0
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 0
+        qu_simparams.to_file("tmp/")
+        test_object = QU_Simparams.from_file("tmp/")
+        assert isinstance(test_object, QU_Simparams)
+        assert qu_simparams == test_object
+
+        # Test stretch grid flag = 1
+        qu_simparams = self.get_test_object()
+        qu_simparams.stretch_grid_flag = 1
+        qu_simparams.custom_dz_array = [1] * qu_simparams.nz
+        qu_simparams.to_file("tmp/")
+        test_object = QU_Simparams.from_file("tmp/")
+        assert isinstance(test_object, QU_Simparams)
+        assert qu_simparams == test_object
+
+
+class TestQFire_Advanced_User_Inputs:
+    def test_init(self):
+        """Test the initialization of a QFire_Advanced_User_Inputs object."""
+        # Test the default initialization
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        assert qfire_advanced_user_inputs.fraction_cells_launch_firebrands == 0.05
+
+        # Test custom initialization
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs(
+            fraction_cells_launch_firebrands=0.1
+        )
+        assert qfire_advanced_user_inputs.fraction_cells_launch_firebrands == 0.1
+
+        # Test data type casting
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs(
+            fraction_cells_launch_firebrands="0.1"
+        )
+        assert isinstance(
+            qfire_advanced_user_inputs.fraction_cells_launch_firebrands, float
+        )
+        assert qfire_advanced_user_inputs.fraction_cells_launch_firebrands == 0.1
+
+        # Pass bad parameters: negative numbers
+        with pytest.raises(ValidationError):
+            QFire_Advanced_User_Inputs(fraction_cells_launch_firebrands=-1)
+
+        # Pass bad parameters: not a fraction
+        with pytest.raises(ValidationError):
+            QFire_Advanced_User_Inputs(fraction_cells_launch_firebrands=2)
+
+        # Pass bad parameters: not a valid range for theta
+        with pytest.raises(ValidationError):
+            QFire_Advanced_User_Inputs(minimum_landing_angle=361)
+
+    def test_to_dict(self):
+        """Test the to_dict method of a QFire_Advanced_User_Inputs object."""
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        result_dict = qfire_advanced_user_inputs.to_dict()
+        assert (
+            result_dict["fraction_cells_launch_firebrands"]
+            == qfire_advanced_user_inputs.fraction_cells_launch_firebrands
+        )
+        assert (
+            result_dict["firebrand_radius_scale_factor"]
+            == qfire_advanced_user_inputs.firebrand_radius_scale_factor
+        )
+        assert (
+            result_dict["firebrand_trajectory_time_step"]
+            == qfire_advanced_user_inputs.firebrand_trajectory_time_step
+        )
+        assert (
+            result_dict["firebrand_launch_interval"]
+            == qfire_advanced_user_inputs.firebrand_launch_interval
+        )
+        assert (
+            result_dict["firebrands_per_deposition"]
+            == qfire_advanced_user_inputs.firebrands_per_deposition
+        )
+        assert (
+            result_dict["firebrand_area_ratio"]
+            == qfire_advanced_user_inputs.firebrand_area_ratio
+        )
+        assert (
+            result_dict["minimum_burn_rate_coefficient"]
+            == qfire_advanced_user_inputs.minimum_burn_rate_coefficient
+        )
+        assert (
+            result_dict["max_firebrand_thickness_fraction"]
+            == qfire_advanced_user_inputs.max_firebrand_thickness_fraction
+        )
+        assert (
+            result_dict["firebrand_germination_delay"]
+            == qfire_advanced_user_inputs.firebrand_germination_delay
+        )
+        assert (
+            result_dict["vertical_velocity_scale_factor"]
+            == qfire_advanced_user_inputs.vertical_velocity_scale_factor
+        )
+        assert (
+            result_dict["minimum_firebrand_ignitions"]
+            == qfire_advanced_user_inputs.minimum_firebrand_ignitions
+        )
+        assert (
+            result_dict["maximum_firebrand_ignitions"]
+            == qfire_advanced_user_inputs.maximum_firebrand_ignitions
+        )
+        assert (
+            result_dict["minimum_landing_angle"]
+            == qfire_advanced_user_inputs.minimum_landing_angle
+        )
+        assert (
+            result_dict["maximum_firebrand_thickness"]
+            == qfire_advanced_user_inputs.maximum_firebrand_thickness
+        )
+
+    def test_from_dict(self):
+        """Test class initialization from a dictionary object"""
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        result_dict = qfire_advanced_user_inputs.to_dict()
+        test_obj = QFire_Advanced_User_Inputs.from_dict(result_dict)
+        assert test_obj == qfire_advanced_user_inputs
+
+    def test_to_docs(self):
+        """Test the to_docs method of a QFire_Advanced_User_Inputs object."""
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        result_dict = qfire_advanced_user_inputs.to_dict()
+        result_docs = qfire_advanced_user_inputs.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        """Test the to_file method of a QFire_Advanced_User_Inputs object."""
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs(
+            fraction_cells_launch_firebrands=0.1
+        )
+        qfire_advanced_user_inputs.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QFIRE_advanced_user_inputs.inp", "r") as file:
+            lines = file.readlines()
+            assert (
+                float(lines[0].strip().split("!")[0])
+                == qfire_advanced_user_inputs.fraction_cells_launch_firebrands
+            )
+            assert (
+                float(lines[1].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrand_radius_scale_factor
+            )
+            assert (
+                float(lines[2].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrand_trajectory_time_step
+            )
+            assert (
+                float(lines[3].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrand_launch_interval
+            )
+            assert (
+                float(lines[4].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrands_per_deposition
+            )
+            assert (
+                float(lines[5].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrand_area_ratio
+            )
+            assert (
+                float(lines[6].strip().split("!")[0])
+                == qfire_advanced_user_inputs.minimum_burn_rate_coefficient
+            )
+            assert (
+                float(lines[7].strip().split("!")[0])
+                == qfire_advanced_user_inputs.max_firebrand_thickness_fraction
+            )
+            assert (
+                float(lines[8].strip().split("!")[0])
+                == qfire_advanced_user_inputs.firebrand_germination_delay
+            )
+            assert (
+                float(lines[9].strip().split("!")[0])
+                == qfire_advanced_user_inputs.vertical_velocity_scale_factor
+            )
+            assert (
+                float(lines[10].strip().split("!")[0])
+                == qfire_advanced_user_inputs.minimum_firebrand_ignitions
+            )
+            assert (
+                float(lines[11].strip().split("!")[0])
+                == qfire_advanced_user_inputs.maximum_firebrand_ignitions
+            )
+            assert (
+                float(lines[12].strip().split("!")[0])
+                == qfire_advanced_user_inputs.minimum_landing_angle
+            )
+            assert (
+                float(lines[13].strip().split("!")[0])
+                == qfire_advanced_user_inputs.maximum_firebrand_thickness
+            )
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            qfire_advanced_user_inputs.to_file(
+                "/non_existent_path/QFIRE_advanced_user_inputs.inp"
+            )
+
+    def test_from_file(self):
+        """Test initializing a class from a QFIRE_advanced_user_inputs.inp
+        file."""
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        qfire_advanced_user_inputs.to_file("tmp/")
+        test_object = QFire_Advanced_User_Inputs.from_file("tmp/")
+        assert isinstance(test_object, QFire_Advanced_User_Inputs)
+        assert qfire_advanced_user_inputs == test_object
+
+
+class TestQUIC_fire:
+    @staticmethod
+    def get_test_object():
+        return QUIC_fire(nz=26, sim_time=60, time_now=1695311421)
+
+    def test_init(self):
+        # Test default initialization
+        quic_fire = self.get_test_object()
+        assert quic_fire.nz == 26
+        assert quic_fire.sim_time == 60
+
+        # Test changing the default values
+        quic_fire.nz = 27
+        assert quic_fire.nz == 27
+
+        # Test data type casting
+        quic_fire = QUIC_fire(nz="26", sim_time=60, time_now=1695311421)
+        assert isinstance(quic_fire.nz, int)
+        assert quic_fire.nz == 26
+
+        # Test stretch grid input
+        assert quic_fire.stretch_grid_flag == 0
+        assert quic_fire.stretch_grid_input == "1"
+        assert quic_fire.dz == 1
+        quic_fire.nz = 5
+        quic_fire.dz_array = [1, 2, 3, 4, 5]
+        quic_fire.stretch_grid_flag = 1
+        assert quic_fire.stretch_grid_input == "1.0\n2.0\n3.0\n4.0\n5.0\n"
+
+        # Test invalid dz array
+        quic_fire = QUIC_fire(
+            nz=26,
+            sim_time=60,
+            time_now=1695311421,
+            stretch_grid_flag=1,
+            dz_array=[1, 2, 3, 4, 5],
+        )
+        with pytest.raises(ValueError):
+            assert quic_fire.stretch_grid_input == "1.0\n2.0\n3.0\n4.0\n5.0\n"
+
+        # Test invalid random_seed
+        quic_fire = self.get_test_object()
+        with pytest.raises(ValidationError):
+            quic_fire.random_seed = 0
+
+        # Test fuel inputs
+        quic_fire = QUIC_fire(nz=26, sim_time=60, time_now=1695311421)
+        assert quic_fire.fuel_density is None
+
+        quic_fire.fuel_flag = 1
+        quic_fire.fuel_density = 0.5
+        quic_fire.fuel_moisture = 1
+        quic_fire.fuel_height = 0.75
+        assert quic_fire.fuel_lines == (
+            f"{quic_fire.fuel_flag}\t! fuel density flag: 1 = uniform; "
+            f"2 = provided thru QF_FuelMoisture.inp, 3 = Firetech"
+            f" files for quic grid, 4 = Firetech files for "
+            f"different grid (need interpolation)"
+            f"\n0.5"
+            f"\n{quic_fire.fuel_flag}\t! fuel moisture flag: 1 = uniform; "
+            f"2 = provided thru QF_FuelMoisture.inp, 3 = Firetech"
+            f" files for quic grid, 4 = Firetech files for "
+            f"different grid (need interpolation)"
+            f"\n1.0"
+            f"\n{quic_fire.fuel_flag}\t! fuel height flag: 1 = uniform; "
+            f"2 = provided thru QF_FuelMoisture.inp, 3 = Firetech"
+            f" files for quic grid, 4 = Firetech files for "
+            f"different grid (need interpolation)"
+            f"\n0.75"
+        )
+
+    def test_to_dict(self):
+        """Test the to_dict method of a QUIC_fire object."""
+        quic_fire = self.get_test_object()
+        quic_fire.ignition_type = RectangleIgnition(
+            x_min=20, y_min=20, x_length=10, y_length=160
+        )
+        result_dict = quic_fire.to_dict()
+
+        assert result_dict["nz"] == quic_fire.nz
+        # TODO: revisit this one
+        assert result_dict["time_now"] == quic_fire.time_now
+        assert result_dict["sim_time"] == quic_fire.sim_time
+        assert result_dict["fire_flag"] == quic_fire.fire_flag
+        assert result_dict["random_seed"] == quic_fire.random_seed
+        assert result_dict["fire_time_step"] == quic_fire.fire_time_step
+        assert result_dict["quic_time_step"] == quic_fire.quic_time_step
+        assert result_dict["out_time_fire"] == quic_fire.out_time_fire
+        assert result_dict["out_time_wind"] == quic_fire.out_time_wind
+        assert result_dict["out_time_emis_rad"] == quic_fire.out_time_emis_rad
+        assert result_dict["out_time_wind_avg"] == quic_fire.out_time_wind_avg
+        assert result_dict["stretch_grid_flag"] == quic_fire.stretch_grid_flag
+        assert result_dict["dz"] == quic_fire.dz
+        assert result_dict["dz_array"] == quic_fire.dz_array
+        assert result_dict["fuel_flag"] == quic_fire.fuel_flag
+        assert result_dict["fuel_density"] == quic_fire.fuel_density
+        assert result_dict["fuel_moisture"] == quic_fire.fuel_moisture
+        assert result_dict["fuel_height"] == quic_fire.fuel_height
+        assert result_dict["fuel_lines"] == quic_fire.fuel_lines
+        assert (
+            result_dict["ignition_type"]["ignition_flag"]
+            == quic_fire.ignition_type.ignition_flag
+        )
+        assert result_dict["ignition_type"]["x_min"] == quic_fire.ignition_type.x_min
+        assert result_dict["ignition_type"]["y_min"] == quic_fire.ignition_type.y_min
+        assert (
+            result_dict["ignition_type"]["x_length"] == quic_fire.ignition_type.x_length
+        )
+        assert (
+            result_dict["ignition_type"]["y_length"] == quic_fire.ignition_type.y_length
+        )
+        assert result_dict["ignitions_per_cell"] == quic_fire.ignitions_per_cell
+        assert result_dict["firebrand_flag"] == quic_fire.firebrand_flag
+        assert result_dict["auto_kill"] == quic_fire.auto_kill
+        assert result_dict["eng_to_atm_out"] == quic_fire.eng_to_atm_out
+        assert result_dict["react_rate_out"] == quic_fire.react_rate_out
+        assert result_dict["fuel_dens_out"] == quic_fire.fuel_dens_out
+        assert result_dict["QF_wind_out"] == quic_fire.QF_wind_out
+        assert result_dict["QU_wind_inst_out"] == quic_fire.QU_wind_inst_out
+        assert result_dict["QU_wind_avg_out"] == quic_fire.QU_wind_avg_out
+        assert result_dict["fuel_moist_out"] == quic_fire.fuel_moist_out
+        assert result_dict["mass_burnt_out"] == quic_fire.mass_burnt_out
+        assert result_dict["firebrand_out"] == quic_fire.firebrand_out
+        assert result_dict["emissions_out"] == quic_fire.emissions_out
+        assert result_dict["radiation_out"] == quic_fire.radiation_out
+        assert result_dict["intensity_out"] == quic_fire.intensity_out
+
+        # Computed fields
+        assert result_dict["stretch_grid_input"] == quic_fire.stretch_grid_input
+        assert result_dict["ignition_lines"] == quic_fire.ignition_lines
+        assert result_dict["fuel_lines"] == quic_fire.fuel_lines
+
+    # TODO: Add test for from_dict
+    # TODO: Add test for to_docs
+    # TODO: Add test for to_file
+
+    def test_from_file(self):
+        """Test initializing a class from a QUIC_fire.inp
+        file."""
+        quic_fire = self.get_test_object()
+        quic_fire.to_file("tmp/")
+        test_object = QUIC_fire.from_file("tmp/")
+        assert isinstance(test_object, QUIC_fire)
+        assert quic_fire == test_object
+
+
+class Test_QFire_Bldg_Advanced_User_Inputs:
+    def test_default_init(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+
+        assert bldg_inputs.convert_buildings_to_fuel_flag == 0
+        assert bldg_inputs.building_fuel_density == 0.5
+        assert bldg_inputs.building_attenuation_coefficient == 2.0
+        assert bldg_inputs.building_surface_roughness == 0.01
+        assert bldg_inputs.convert_fuel_to_canopy_flag == 1
+        assert bldg_inputs.update_canopy_winds_flag == 1
+        assert bldg_inputs.fuel_attenuation_coefficient == 1.0
+        assert bldg_inputs.fuel_surface_roughness == 0.1
+
+    def test_custom_init(self):
+        # Change a flag
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs(convert_buildings_to_fuel_flag=1)
+        assert bldg_inputs.convert_buildings_to_fuel_flag == 1
+
+        # Change a float
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs(building_fuel_density=0.6)
+        assert bldg_inputs.building_fuel_density == 0.6
+
+        # Test data type casting
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs(building_fuel_density="0.6")
+        assert isinstance(bldg_inputs.building_fuel_density, float)
+
+    def test_init_invalid_values(self):
+        # Test invalid convert_buildings_to_fuel_flag
+        for invalid_flag in [-1, 2, "1", 1.0, 1.5]:
+            with pytest.raises(ValidationError):
+                QFire_Bldg_Advanced_User_Inputs(
+                    convert_buildings_to_fuel_flag=invalid_flag
+                )
+
+        # Test invalid building_fuel_density
+        for invalid_density in [-1, ""]:
+            with pytest.raises(ValidationError):
+                QFire_Bldg_Advanced_User_Inputs(building_fuel_density=invalid_density)
+
+    def test_to_dict(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        result_dict = bldg_inputs.to_dict()
+
+        assert (
+            result_dict["convert_buildings_to_fuel_flag"]
+            == bldg_inputs.convert_buildings_to_fuel_flag
+        )
+        assert result_dict["building_fuel_density"] == bldg_inputs.building_fuel_density
+        assert (
+            result_dict["building_attenuation_coefficient"]
+            == bldg_inputs.building_attenuation_coefficient
+        )
+        assert (
+            result_dict["building_surface_roughness"]
+            == bldg_inputs.building_surface_roughness
+        )
+        assert (
+            result_dict["convert_fuel_to_canopy_flag"]
+            == bldg_inputs.convert_fuel_to_canopy_flag
+        )
+        assert (
+            result_dict["update_canopy_winds_flag"]
+            == bldg_inputs.update_canopy_winds_flag
+        )
+        assert (
+            result_dict["fuel_attenuation_coefficient"]
+            == bldg_inputs.fuel_attenuation_coefficient
+        )
+        assert (
+            result_dict["fuel_surface_roughness"] == bldg_inputs.fuel_surface_roughness
+        )
+
+    def test_from_dict(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        result_dict = bldg_inputs.to_dict()
+        test_obj = QFire_Bldg_Advanced_User_Inputs.from_dict(result_dict)
+        assert test_obj == bldg_inputs
+
+    def test_to_docs(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        result_dict = bldg_inputs.to_dict()
+        result_docs = bldg_inputs.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        bldg_inputs.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QFIRE_bldg_advanced_user_inputs.inp", "r") as file:
+            lines = file.readlines()
+        assert (
+            int(lines[0].strip().split("!")[0])
+            == bldg_inputs.convert_buildings_to_fuel_flag
+        )
+        assert (
+            float(lines[1].strip().split("!")[0]) == bldg_inputs.building_fuel_density
+        )
+        assert (
+            float(lines[2].strip().split("!")[0])
+            == bldg_inputs.building_attenuation_coefficient
+        )
+        assert (
+            float(lines[3].strip().split("!")[0])
+            == bldg_inputs.building_surface_roughness
+        )
+        assert (
+            int(lines[4].strip().split("!")[0])
+            == bldg_inputs.convert_fuel_to_canopy_flag
+        )
+        assert (
+            int(lines[5].strip().split("!")[0]) == bldg_inputs.update_canopy_winds_flag
+        )
+        assert (
+            float(lines[6].strip().split("!")[0])
+            == bldg_inputs.fuel_attenuation_coefficient
+        )
+        assert (
+            float(lines[7].strip().split("!")[0]) == bldg_inputs.fuel_surface_roughness
+        )
+
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            bldg_inputs.to_file(
+                "/non_existent_path/QFIRE_bldg_advanced_user_inputs.inp"
+            )
+
+    def test_from_file(self):
+        bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
+        bldg_inputs.to_file("tmp/")
+        test_object = QFire_Bldg_Advanced_User_Inputs.from_file("tmp/")
+        assert isinstance(test_object, QFire_Bldg_Advanced_User_Inputs)
+        assert bldg_inputs == test_object
+
+
+class Test_QFire_Plume_Advanced_User_Inputs:
+    def test_default_init(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+
+        assert plume_inputs.max_plumes_per_timestep == 150000
+        assert plume_inputs.min_plume_updraft_velocity == 0.1
+        assert plume_inputs.max_plume_updraft_velocity == 100.0
+        assert plume_inputs.min_velocity_ratio == 0.1
+        assert plume_inputs.brunt_vaisala_freq_squared == 0.0
+        assert plume_inputs.creeping_flag == 1
+        assert plume_inputs.adaptive_timestep_flag == 0
+        assert plume_inputs.plume_timestep == 1.0
+        assert plume_inputs.sor_option_flag == 1
+        assert plume_inputs.sor_alpha_plume_center == 10.0
+        assert plume_inputs.sor_alpha_plume_edge == 1.0
+        assert plume_inputs.max_plume_merging_angle == 30.0
+        assert plume_inputs.max_plume_overlap_fraction == 0.7
+        assert plume_inputs.plume_to_grid_updrafts_flag == 1
+        assert plume_inputs.max_points_along_plume_edge == 10
+        assert plume_inputs.plume_to_grid_intersection_flag == 1
+
+    def test_custom_init(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs(
+            max_plumes_per_timestep=100000,
+            min_plume_updraft_velocity=0.2,
+            creeping_flag=0,
+            max_plume_updraft_velocity="100.",
+        )
+        assert plume_inputs.max_plumes_per_timestep == 100000
+        assert plume_inputs.min_plume_updraft_velocity == 0.2
+        assert plume_inputs.creeping_flag == 0
+        assert plume_inputs.max_plume_updraft_velocity == 100.0
+
+    def test_invalid_values(self):
+        # Invalid max_plumes_per_timestep (Positive integer)
+        for value in [-1, 0, 1.5, "a"]:
+            with pytest.raises(ValidationError):
+                QFire_Plume_Advanced_User_Inputs(max_plumes_per_timestep=value)
+
+        # Invalid min_plume_updraft_velocity (Positive float)
+        for value in [-1, 0, "a"]:
+            with pytest.raises(ValidationError):
+                QFire_Plume_Advanced_User_Inputs(min_plume_updraft_velocity=value)
+
+        # Invalid brunt_vaisala_freq_squared (Non-negative float)
+        for value in [-1, "a"]:
+            with pytest.raises(ValidationError):
+                QFire_Plume_Advanced_User_Inputs(brunt_vaisala_freq_squared=value)
+
+        # Invalid creeping_flag (Literal 0 or 1)
+        for value in [-1, 2, 1.5, "a", "0"]:
+            with pytest.raises(ValidationError):
+                QFire_Plume_Advanced_User_Inputs(creeping_flag=value)
+
+    def test_to_dict(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        result_dict = plume_inputs.to_dict()
+
+        assert (
+            result_dict["max_plumes_per_timestep"]
+            == plume_inputs.max_plumes_per_timestep
+        )
+        assert (
+            result_dict["min_plume_updraft_velocity"]
+            == plume_inputs.min_plume_updraft_velocity
+        )
+        assert (
+            result_dict["max_plume_updraft_velocity"]
+            == plume_inputs.max_plume_updraft_velocity
+        )
+        assert result_dict["min_velocity_ratio"] == plume_inputs.min_velocity_ratio
+        assert (
+            result_dict["brunt_vaisala_freq_squared"]
+            == plume_inputs.brunt_vaisala_freq_squared
+        )
+        assert result_dict["creeping_flag"] == plume_inputs.creeping_flag
+        assert (
+            result_dict["adaptive_timestep_flag"] == plume_inputs.adaptive_timestep_flag
+        )
+        assert result_dict["plume_timestep"] == plume_inputs.plume_timestep
+        assert result_dict["sor_option_flag"] == plume_inputs.sor_option_flag
+        assert (
+            result_dict["sor_alpha_plume_center"] == plume_inputs.sor_alpha_plume_center
+        )
+        assert result_dict["sor_alpha_plume_edge"] == plume_inputs.sor_alpha_plume_edge
+        assert (
+            result_dict["max_plume_merging_angle"]
+            == plume_inputs.max_plume_merging_angle
+        )
+        assert (
+            result_dict["max_plume_overlap_fraction"]
+            == plume_inputs.max_plume_overlap_fraction
+        )
+        assert (
+            result_dict["plume_to_grid_updrafts_flag"]
+            == plume_inputs.plume_to_grid_updrafts_flag
+        )
+        assert (
+            result_dict["max_points_along_plume_edge"]
+            == plume_inputs.max_points_along_plume_edge
+        )
+        assert (
+            result_dict["plume_to_grid_intersection_flag"]
+            == plume_inputs.plume_to_grid_intersection_flag
+        )
+
+    def test_from_dict(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        result_dict = plume_inputs.to_dict()
+        test_obj = QFire_Plume_Advanced_User_Inputs.from_dict(result_dict)
+        assert test_obj == plume_inputs
+
+    def test_to_docs(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        result_dict = plume_inputs.to_dict()
+        result_docs = plume_inputs.get_documentation()
+        for key in result_dict:
+            assert key in result_docs
+        for key in result_docs:
+            assert key in result_dict
+
+    def test_to_file(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        plume_inputs.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QFIRE_plume_advanced_user_inputs.inp", "r") as file:
+            lines = file.readlines()
+        assert (
+            int(lines[0].strip().split("!")[0]) == plume_inputs.max_plumes_per_timestep
+        )
+        assert (
+            float(lines[1].strip().split("!")[0])
+            == plume_inputs.min_plume_updraft_velocity
+        )
+        assert (
+            float(lines[2].strip().split("!")[0])
+            == plume_inputs.max_plume_updraft_velocity
+        )
+        assert float(lines[3].strip().split("!")[0]) == plume_inputs.min_velocity_ratio
+        assert (
+            float(lines[4].strip().split("!")[0])
+            == plume_inputs.brunt_vaisala_freq_squared
+        )
+        assert int(lines[5].strip().split("!")[0]) == plume_inputs.creeping_flag
+        assert (
+            int(lines[6].strip().split("!")[0]) == plume_inputs.adaptive_timestep_flag
+        )
+        assert float(lines[7].strip().split("!")[0]) == plume_inputs.plume_timestep
+        assert int(lines[8].strip().split("!")[0]) == plume_inputs.sor_option_flag
+        assert (
+            float(lines[9].strip().split("!")[0]) == plume_inputs.sor_alpha_plume_center
+        )
+        assert (
+            float(lines[10].strip().split("!")[0]) == plume_inputs.sor_alpha_plume_edge
+        )
+        assert (
+            float(lines[11].strip().split("!")[0])
+            == plume_inputs.max_plume_merging_angle
+        )
+        assert (
+            float(lines[12].strip().split("!")[0])
+            == plume_inputs.max_plume_overlap_fraction
+        )
+        assert (
+            int(lines[13].strip().split("!")[0])
+            == plume_inputs.plume_to_grid_updrafts_flag
+        )
+        assert (
+            int(lines[14].strip().split("!")[0])
+            == plume_inputs.max_points_along_plume_edge
+        )
+        assert (
+            int(lines[15].strip().split("!")[0])
+            == plume_inputs.plume_to_grid_intersection_flag
+        )
+
+    def test_from_file(self):
+        plume_inputs = QFire_Plume_Advanced_User_Inputs()
+        plume_inputs.to_file("tmp/")
+        test_object = QFire_Plume_Advanced_User_Inputs.from_file("tmp/")
+        assert isinstance(test_object, QFire_Plume_Advanced_User_Inputs)
+        assert plume_inputs == test_object
+
+
+class TestQUTopoInputs:
+    def get_default_test_object(self):
+        return QU_TopoInputs(topo_type=TopoType(topo_flag=0))
+
+    def get_complex_test_object(self):
+        return QU_TopoInputs(
+            topo_type=GaussianHillTopo(
+                x_hilltop=100, y_hilltop=150, elevation_max=500, elevation_std=20
+            ),
+            smoothing_method=1,
+            sor_relax=1.78,
+        )
+
+    def test_default_inputs(self):
+        topoinputs = self.get_default_test_object()
+        assert topoinputs.filename == "topo.dat"
+        assert isinstance(topoinputs.topo_type, TopoType)
+        assert topoinputs.smoothing_passes == 500
+
+    def test_complex_inputs(self):
+        topoinputs = self.get_complex_test_object()
+        assert topoinputs.topo_type.topo_flag.value == 1
+        assert topoinputs.topo_lines == (
+            "1\t\t! N/A, "
+            "topo flag: 0 = flat, 1 = Gaussian hill, "
+            "2 = hill pass, 3 = slope mesa, 4 = canyon, "
+            "5 = custom, 6 = half circle, 7 = sinusoid, "
+            "8 = cos hill, 9 = QP_elevation.inp, "
+            "10 = terrainOutput.txt (ARA), "
+            "11 = terrain.dat (firetec)\n"
+            "100\t! m, x-center\n"
+            "150\t! m, y-center\n"
+            "500\t! m, max height\n"
+            "20.0\t! m, std"
+        )
+        assert topoinputs.smoothing_method == 1
+        assert topoinputs.sor_relax == 1.78
+
+    def test_to_dict(self):
+        topoinputs = self.get_default_test_object()
+        test_dict = topoinputs.to_dict()
+        assert test_dict["smoothing_method"] == topoinputs.smoothing_method
+        assert test_dict["smoothing_passes"] == topoinputs.smoothing_passes
+        assert test_dict["sor_iterations"] == topoinputs.sor_iterations
+        assert test_dict["sor_cycles"] == topoinputs.sor_cycles
+        assert test_dict["sor_relax"] == topoinputs.sor_relax
+
+        topoinputs = self.get_complex_test_object()
+        test_dict = topoinputs.to_dict()
+        assert test_dict["topo_type"]["x_hilltop"] == topoinputs.topo_type.x_hilltop
+        assert test_dict["topo_type"]["y_hilltop"] == topoinputs.topo_type.y_hilltop
+        assert (
+            test_dict["topo_type"]["elevation_max"]
+            == topoinputs.topo_type.elevation_max
+        )
+        assert (
+            test_dict["topo_type"]["elevation_std"]
+            == topoinputs.topo_type.elevation_std
+        )
+        assert test_dict["smoothing_method"] == topoinputs.smoothing_method
+        assert test_dict["smoothing_passes"] == topoinputs.smoothing_passes
+        assert test_dict["sor_iterations"] == topoinputs.sor_iterations
+        assert test_dict["sor_cycles"] == topoinputs.sor_cycles
+        assert test_dict["sor_relax"] == topoinputs.sor_relax
+
+    def test_from_dict(self):
+        topoinputs = self.get_default_test_object()
+        result_dict = topoinputs.to_dict()
+        test_obj = QU_TopoInputs.from_dict(result_dict)
+        assert test_obj == topoinputs
+
+    def test_to_file(self):
+        topoinputs = self.get_default_test_object()
+        topoinputs.to_file("tmp/")
+        with open("tmp/QU_TopoInputs.inp", "r") as file:
+            lines = file.readlines()
+        topo_flag = int(lines[2].strip().split("!")[0])
+        assert topo_flag == topoinputs.topo_type.topo_flag.value
+        add_dict = {
+            0: 0,
+            1: 4,
+            2: 2,
+            3: 3,
+            4: 5,
+            5: 0,
+            6: 3,
+            7: 2,
+            8: 2,
+            9: 0,
+            10: 0,
+            11: 0,
+        }
+        add = add_dict.get(topo_flag)
+        current_line = current_line = 3 + add
+        assert (
+            int(lines[current_line].strip().split("!")[0])
+            == topoinputs.smoothing_method
+        )
+        assert (
+            int(lines[current_line + 1].strip().split("!")[0])
+            == topoinputs.smoothing_passes
+        )
+        assert (
+            int(lines[current_line + 2].strip().split("!")[0])
+            == topoinputs.sor_iterations
+        )
+        assert (
+            int(lines[current_line + 3].strip().split("!")[0]) == topoinputs.sor_cycles
+        )
+        assert (
+            float(lines[current_line + 4].strip().split("!")[0]) == topoinputs.sor_relax
+        )
+
+        topoinputs = self.get_complex_test_object()
+        topoinputs.to_file("tmp/")
+        with open("tmp/QU_TopoInputs.inp", "r") as file:
+            lines = file.readlines()
+        topo_flag = int(lines[2].strip().split("!")[0])
+        assert topo_flag == topoinputs.topo_type.topo_flag.value
+        add_dict = {
+            0: 0,
+            1: 4,
+            2: 2,
+            3: 3,
+            4: 5,
+            5: 0,
+            6: 3,
+            7: 2,
+            8: 2,
+            9: 0,
+            10: 0,
+            11: 0,
+        }
+        add = add_dict.get(topo_flag)
+        assert int(lines[3].strip().split("!")[0]) == topoinputs.topo_type.x_hilltop
+        assert int(lines[4].strip().split("!")[0]) == topoinputs.topo_type.y_hilltop
+        assert int(lines[5].strip().split("!")[0]) == topoinputs.topo_type.elevation_max
+        assert (
+            float(lines[6].strip().split("!")[0]) == topoinputs.topo_type.elevation_std
+        )
+        current_line = current_line = 3 + add
+        assert (
+            int(lines[current_line].strip().split("!")[0])
+            == topoinputs.smoothing_method
+        )
+        assert (
+            int(lines[current_line + 1].strip().split("!")[0])
+            == topoinputs.smoothing_passes
+        )
+        assert (
+            int(lines[current_line + 2].strip().split("!")[0])
+            == topoinputs.sor_iterations
+        )
+        assert (
+            int(lines[current_line + 3].strip().split("!")[0]) == topoinputs.sor_cycles
+        )
+        assert (
+            float(lines[current_line + 4].strip().split("!")[0]) == topoinputs.sor_relax
+        )
+
+    def test_from_file(self):
+        topoinputs = self.get_default_test_object()
+        topoinputs.to_file("tmp/")
+        test_object = QU_TopoInputs.from_file("tmp/")
+        assert isinstance(test_object, QU_TopoInputs)
+        assert topoinputs == test_object
+
+        topoinputs = self.get_complex_test_object()
+        topoinputs.to_file("tmp/")
+        test_object = QU_TopoInputs.from_file("tmp/")
+        assert isinstance(test_object, QU_TopoInputs)
+        assert topoinputs == test_object
+
+
+class TestRuntimeAdvancedUserInputs:
+    def get_test_object(self):
+        return RuntimeAdvancedUserInputs()
+
+    def test_init(self):
+        raui = self.get_test_object()
+        assert isinstance(raui, RuntimeAdvancedUserInputs)
+        assert raui.num_cpus == 8
+        assert raui.use_acw == 0
+
+    def test_from_file(self):
+        raui = self.get_test_object()
+        raui.to_file("tmp/")
+        test_object = RuntimeAdvancedUserInputs.from_file("tmp/")
+        assert raui == test_object
+
+
+class TestQUmovingcoords:
+    def get_test_object(self):
+        return QU_movingcoords()
+
+    def test_init(self):
+        qu_moving = self.get_test_object()
+        assert isinstance(qu_moving, QU_movingcoords)
+
+    def test_from_file(self):
+        qu_moving = self.get_test_object()
+        qu_moving.to_file("tmp/")
+        test_object = QU_movingcoords.from_file("tmp/")
+        assert qu_moving == test_object
+
+
+class TestQUbuildout:
+    def get_test_object(self):
+        return QP_buildout()
+
+    def test_init(self):
+        qp_buildout = self.get_test_object()
+        assert isinstance(qp_buildout, QP_buildout)
+
+    def test_from_file(self):
+        qp_buildout = self.get_test_object()
+        qp_buildout.to_file("tmp/")
+        test_object = QP_buildout.from_file("tmp/")
+        assert qp_buildout == test_object
+
+
+class Test_QU_metparams:
+    def get_test_object(self):
+        return QU_metparams()
+
+    def test_init(self):
+        qu_metparams = self.get_test_object()
+        assert qu_metparams.num_sensors == 1
+        assert qu_metparams.sensor_name == "sensor1"
+
+    def test_to_dict(self):
+        qu_metparams = self.get_test_object()
+        result_dict = qu_metparams.to_dict()
+        assert result_dict["num_sensors"] == qu_metparams.num_sensors
+        assert result_dict["sensor_name"] == qu_metparams.sensor_name
+
+    def test_from_dict(self):
+        qu_metparams = self.get_test_object()
+        result_dict = qu_metparams.to_dict()
+        test_obj = QU_metparams.from_dict(result_dict)
+        assert test_obj == qu_metparams
+
+    def test_to_file(self):
+        qu_metparams = self.get_test_object()
+        qu_metparams.to_file("tmp/")
+
+        # Read the content of the file and check for correctness
+        with open("tmp/QU_metparams.inp", "r") as file:
+            lines = file.readlines()
+        assert int(lines[2].strip().split("!")[0]) == qu_metparams.num_sensors
+        assert str(lines[4].strip().split("!")[0].strip()) == qu_metparams.sensor_name
+
+    def test_from_file(self):
+        qu_metparams = self.get_test_object()
+        qu_metparams.to_file("tmp/")
+        test_object = QU_metparams.from_file("tmp/")
+        assert isinstance(test_object, QU_metparams)
+        assert qu_metparams == test_object
+
+
+class TestSensor1:
+    def get_test_object(self):
+        return Sensor1(time_now=1697555154, wind_speed=5, wind_direction=270)
+
+    def test_init(self):
+        sensor1 = self.get_test_object()
+        assert isinstance(sensor1, Sensor1)
+        assert sensor1.wind_speed == 5.0
+        assert sensor1.wind_direction == 270
+        assert sensor1.sensor_height == 6.1
+
+    def test_error(self):
+        sensor1 = self.get_test_object()
+        with pytest.raises(ValidationError):
+            sensor1.wind_direction = 360
+
+    def test_from_file(self):
+        sensor1 = self.get_test_object()
+        sensor1.to_file("tmp/")
+        test_object = Sensor1.from_file("tmp/")
+        assert sensor1 == test_object
+
+
+class TestSimulationInputs:
+    @staticmethod
+    def get_basic_test_object():
+        return SimulationInputs.setup_simulation(
+            nx=100,
+            ny=100,
+            fire_nz=40,
+            quic_nz=26,
+            quic_height=180,
+            dx=2,
+            dy=2,
+            fire_dz=1,
+            wind_speed=2.7,
+            wind_direction=270,
+            simulation_time=600,
+            output_time=60,
+        )
+
+    def test_basic_inputs(self):
+        sim_inputs = self.get_basic_test_object()
+        assert isinstance(sim_inputs, SimulationInputs)
+
+    def test_list_inputs(self):
+        sim_inputs = self.get_basic_test_object()
+        inputs = sim_inputs.list_inputs()
+        assert "rasterorigin" in inputs
+
+    def test_get_input(self):
+        sim_inputs = self.get_basic_test_object()
+        rasterorigin = sim_inputs.get_input("rasterorigin")
+        assert isinstance(rasterorigin, RasterOrigin)
+
+    def test_write_inputs(self):
+        sim_inputs = self.get_basic_test_object()
+        sim_inputs.write_inputs("tmp/")
 
-TEST_PARAMS = {
-    "nx": 100,
-    "ny": 100,
-    "nz": 1,
-    "dx": 1.,
-    "dy": 1.,
-    "dz": 1.,
-    "wind_speed": 4.,
-    "wind_direction": 265,
-    "sim_time": 60,
-    "auto_kill": 1,
-    "num_cpus": 1,
-    "fuel_flag": 3,
-    "ignition_flag": 1,
-    "output_time": 10,
-}
-
-SUT = SimulationInputs("pytest-simulation")
-
-
-def test_invalid_input_parameters():
-    # Test missing nx parameter
-    test_custom_params = TEST_PARAMS.copy()
-    _ = test_custom_params.pop("nx")
-    with pytest.raises(KeyError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-integer nx parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["nx"] = 1.0
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string nx parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["nx"] = "10"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test nx parameter less than 1
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["nx"] = 0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string dx parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["dx"] = "1.0"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test dx parameter less than 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["dx"] = -1.0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test dx parameter equal to 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["dx"] = 0.0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test missing wind_speed parameter
-    test_custom_params = TEST_PARAMS.copy()
-    _ = test_custom_params.pop("wind_speed")
-    with pytest.raises(KeyError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string wind_speed parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["wind_speed"] = "4.0"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test wind_speed parameter less than 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["wind_speed"] = -4.0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test wind_direction string parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["wind_direction"] = "265"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test wind_direction parameter less than 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["wind_direction"] = -265
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test wind_direction parameter greater than 360
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["wind_direction"] = 365
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test missing sim_time parameter
-    test_custom_params = TEST_PARAMS.copy()
-    _ = test_custom_params.pop("sim_time")
-    with pytest.raises(KeyError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string sim_time parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["sim_time"] = "60"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test float sim_time parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["sim_time"] = 60.0
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test sim_time parameter less than 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["sim_time"] = -60
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test sim_time parameter equal to 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["sim_time"] = 0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string auto_kill parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["auto_kill"] = "1"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test auto_kill parameter not 0 or 1
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["auto_kill"] = 2
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string num_cpus parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["num_cpus"] = "1"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test num_cpus parameter less than 1
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["num_cpus"] = 0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-integer output_time parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["output_time"] = 1.0
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string output_time parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["output_time"] = "1"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test output_time parameter less than 1
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["output_time"] = 0
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-integer fuel flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["fuel_flag"] = 1.0
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-supported fuel flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["fuel_flag"] = 2
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test fuel flag 1 has fuel_density, fuel_moisture, fuel_height parameters
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["fuel_flag"] = 1
-    with pytest.raises(KeyError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test fuel flag 1 string fuel_density parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["fuel_flag"] = 1
-    test_custom_params["fuel_density"] = "1.0"
-    test_custom_params["fuel_moisture"] = 0.5
-    test_custom_params["fuel_height"] = 0.5
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test fuel flag 1 fuel_density parameter less than 0
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["fuel_flag"] = 1
-    test_custom_params["fuel_density"] = -1.0
-    test_custom_params["fuel_moisture"] = 0.5
-    test_custom_params["fuel_height"] = 0.5
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test string ignition_flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["ignition_flag"] = "1"
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-supported ignition_flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["ignition_flag"] = 2
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-integer topo_flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["topo_flag"] = 1.0
-    with pytest.raises(TypeError):
-        SUT.setup_input_files(test_custom_params)
-
-    # Test non-supported topo_flag parameter
-    test_custom_params = TEST_PARAMS.copy()
-    test_custom_params["topo_flag"] = 2
-    with pytest.raises(ValueError):
-        SUT.setup_input_files(test_custom_params)
-
-
-# TODO: Update fuel height test
-def test_write_fuel_data():
-    # Test fuel flag 1
-    fuel_data_params = TEST_PARAMS.copy()
-    fuel_data_params["fuel_flag"] = 1
-    fuel_data_params["fuel_density"] = 1.25
-    fuel_data_params["fuel_moisture"] = 0.75
-    fuel_data_params["fuel_height"] = 0.5
-    fuel_params = SUT._write_fuel(fuel_data_params)
-    assert fuel_params == ("\n1.25", "\n0.75", "\n0.5")
-
-    # Test fuel flag 1
-    fuel_data_params = TEST_PARAMS.copy()
-    fuel_data_params["fuel_flag"] = 1
-    fuel_data_params["fuel_density"] = 0.0
-    fuel_data_params["fuel_moisture"] = 0.0
-    fuel_data_params["fuel_height"] = 0.0
-    fuel_params = SUT._write_fuel(fuel_data_params)
-    assert fuel_params == ("\n0.0", "\n0.0", "\n0.0")
-
-    # Test fuel flag 3
-    fuel_data_params = TEST_PARAMS.copy()
-    fuel_data_params["fuel_flag"] = 3
-    fuel_params = SUT._write_fuel(fuel_data_params)
-    assert fuel_params == ("", "", "")
-
-    # Test fuel flag 3 with spurious fuel data
-    fuel_data_params = TEST_PARAMS.copy()
-    fuel_data_params["fuel_flag"] = 3
-    fuel_data_params["fuel_density"] = 1.25
-    fuel_data_params["fuel_moisture"] = 0.75
-    fuel_data_params["fuel_height"] = 0.5
-    fuel_params = SUT._write_fuel(fuel_data_params)
-    assert fuel_params == ("", "", "")
-
-    # Test fuel flag 4
-    fuel_data_params = TEST_PARAMS.copy()
-    fuel_data_params["fuel_flag"] = 4
-    fuel_params = SUT._write_fuel(fuel_data_params)
-    assert fuel_params == ("", "", "")
-
-
-def test_write_ignition_locations():
-    # Test custom ignition flag 6
-    ignition_custom_params = TEST_PARAMS.copy()
-    ignition_custom_params["ignition_flag"] = 6
-    assert SUT._write_ignition_locations(ignition_custom_params) == ""
-
-
-def test_write_line_fire_ignition():
-    ignition_line_params = TEST_PARAMS.copy()
-    ignition_line_params["ignition_flag"] = 1
-
-    # Define default ignition line locations
-    north_wind = "\n10\n89\n80\n1\n100"
-    east_wind = "\n89\n10\n1\n80\n100"
-    south_wind = "\n10\n9\n80\n1\n100"
-    west_wind = "\n9\n10\n1\n80\n100"
-
-    # Test north wind
-    ignition_line_params["wind_direction"] = 0
-    assert SUT._write_line_fire_ignition(ignition_line_params) == north_wind
-
-    # Test east wind
-    ignition_line_params["wind_direction"] = 90
-    assert SUT._write_line_fire_ignition(ignition_line_params) == east_wind
-
-    # Test south wind
-    ignition_line_params["wind_direction"] = 180
-    assert SUT._write_line_fire_ignition(ignition_line_params) == south_wind
-
-    # Test west wind
-    ignition_line_params["wind_direction"] = 270
-    assert SUT._write_line_fire_ignition(ignition_line_params) == west_wind
-
-    # Test 45 degree wind
-    ignition_line_params["wind_direction"] = 45
-    assert SUT._write_line_fire_ignition(ignition_line_params) == north_wind
-
-    # Test 45.01 degree wind
-    ignition_line_params["wind_direction"] = 45.01
-    assert SUT._write_line_fire_ignition(ignition_line_params) == east_wind
-
-    # Test -45 degree wind
-    ignition_line_params["wind_direction"] = -45
-    assert SUT._write_line_fire_ignition(ignition_line_params) == north_wind
-
-    # Test -45.01 degree wind
-    ignition_line_params["wind_direction"] = -45.01
-    assert SUT._write_line_fire_ignition(ignition_line_params) == west_wind
-
-    # Test 134.99 degree wind
-    ignition_line_params["wind_direction"] = 134.99
-    assert SUT._write_line_fire_ignition(ignition_line_params) == east_wind
-
-    # Test 135 degree wind
-    ignition_line_params["wind_direction"] = 135
-    assert SUT._write_line_fire_ignition(ignition_line_params) == south_wind
+    def test_from_directory(self):
+        sim_inputs = SimulationInputs.from_directory("tmp/")
+        assert isinstance(sim_inputs, SimulationInputs)
diff --git a/tests/test_output_module.py b/tests/test_output_module.py
index a7f4254..ddc8935 100644
--- a/tests/test_output_module.py
+++ b/tests/test_output_module.py
@@ -1,17 +1,17 @@
-import sys
-
-sys.path.append("../quicfire_tools")
-from quicfire_tools import outputs
-from quicfire_tools.parameters import SimulationParameters
+"""
+Test module for quickfire_tools.outputs
+"""
+from pathlib import Path
 
-import zarr
-import pytest
 import numpy as np
+import pytest
+import zarr
 import xarray as xr
 import dask.array as da
 from scipy.io import FortranFile
 
-from pathlib import Path
+from quicfire_tools import outputs
+from quicfire_tools.parameters import SimulationParameters
 
 DATA_PATH = Path("data")
 SIMULATION_PATH = DATA_PATH / "crazy-canyon-simulation"
@@ -68,8 +68,9 @@ def test_zarr_single_output(self):
         Run a test to ensure that the zarr output contains a single output
         """
         single_output_name = "mburnt_integ"
-        zarr_with_datasets = self.sut.to_zarr("tmp/test.zarr",
-                                              outputs=single_output_name)
+        zarr_with_datasets = self.sut.to_zarr(
+            "tmp/test.zarr", outputs=single_output_name
+        )
         assert isinstance(zarr_with_datasets, zarr.hierarchy.Group)
         for output_name in self.sut.list_available_outputs():
             output = self.sut.get_output(output_name)
@@ -85,8 +86,9 @@ def test_zarr_multiple_outputs(self):
         Run a test to ensure that the zarr output contains multiple outputs
         """
         multiple_output_names = ["mburnt_integ", "fuels-dens"]
-        zarr_with_datasets = self.sut.to_zarr("tmp/test.zarr",
-                                              outputs=multiple_output_names)
+        zarr_with_datasets = self.sut.to_zarr(
+            "tmp/test.zarr", outputs=multiple_output_names
+        )
         assert isinstance(zarr_with_datasets, zarr.hierarchy.Group)
         for output_name in self.sut.list_available_outputs():
             output = self.sut.get_output(output_name)
@@ -108,23 +110,31 @@ def test_zarr_xarray_connection(self):
         # Test: All outputs.
         # Produces 4D xarray dataset with all 3D outputs
         self.sut.to_zarr("tmp/test.zarr")
-        ds = xr.open_zarr("tmp/test.zarr", drop_variables=["groundfuelheight", "mburnt_integ"])
+        ds = xr.open_zarr(
+            "tmp/test.zarr", drop_variables=["groundfuelheight", "mburnt_integ"]
+        )
         print(ds)
 
         # Test: Single output
         # Produces 4D xarray dataset with single 2D output
         single_output_name = "mburnt_integ"
         self.sut.to_zarr("tmp/test.zarr", outputs=single_output_name)
-        drop_variables = [output_name for output_name in self.sut.list_available_outputs() if
-                          output_name != single_output_name]
+        drop_variables = [
+            output_name
+            for output_name in self.sut.list_available_outputs()
+            if output_name != single_output_name
+        ]
         ds = xr.open_zarr("tmp/test.zarr", drop_variables=drop_variables)
         print(ds)
 
         # Test: Multiple outputs different dimensions causes error
         multiple_output_names = ["mburnt_integ", "fuels-dens"]
         self.sut.to_zarr("tmp/test.zarr", outputs=multiple_output_names)
-        drop_variables = [output_name for output_name in self.sut.list_available_outputs() if
-                          output_name not in multiple_output_names]
+        drop_variables = [
+            output_name
+            for output_name in self.sut.list_available_outputs()
+            if output_name not in multiple_output_names
+        ]
         with pytest.raises(ValueError):
             xr.open_zarr("tmp/test.zarr", drop_variables=drop_variables)
 
@@ -162,15 +172,13 @@ def test_get_multiple_timesteps(self):
         output = self.simulation_outputs.get_output("fire-energy_to_atmos")
         output_path = output.filepaths
         data = output._get_multiple_timesteps(output_path)
-        assert data.shape == (
-            2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
+        assert data.shape == (2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
 
         # Test 3: 3D Compressed data (all timesteps)
         output = self.simulation_outputs.get_output("fuels-dens")
         output_path = output.filepaths
         data = output._get_multiple_timesteps(output_path)
-        assert data.shape == (
-            2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
+        assert data.shape == (2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
 
     def test_to_numpy(self):
         # Test 1: 2D gridded data (single timestep)
@@ -191,14 +199,12 @@ def test_to_numpy(self):
         # Test 4: 3D gridded data (all timesteps)
         output = self.simulation_outputs.get_output("fire-energy_to_atmos")
         data = self.simulation_outputs.to_numpy(output)
-        assert data.shape == (
-            2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
+        assert data.shape == (2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
 
         # Test 5: 3D gridded data (list timesteps)
         output = self.simulation_outputs.get_output("fire-energy_to_atmos")
         data = self.simulation_outputs.to_numpy(output, [0, 1])
-        assert data.shape == (
-            2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
+        assert data.shape == (2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
 
         # Test 6: 3D gridded data (invalid timesteps)
         output = self.simulation_outputs.get_output("fire-energy_to_atmos")
@@ -213,8 +219,7 @@ def test_to_numpy(self):
         # Test 8: 3D compressed data (all timesteps)
         output = self.simulation_outputs.get_output("fuels-dens")
         data = self.simulation_outputs.to_numpy(output)
-        assert data.shape == (
-            2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
+        assert data.shape == (2, SIM_PARAMS.ny, SIM_PARAMS.nx, SIM_PARAMS.nz + 1)
 
 
 class TestProcessGriddedBinSlice:
@@ -247,7 +252,8 @@ def _assert_data_equality(self, data1, data2):
 
     def test_groundfuelheight(self):
         input_data = self._load_and_process_input(
-            SIMULATION_PATH / "treesfueldepth.dat")
+            SIMULATION_PATH / "treesfueldepth.dat"
+        )
 
         output_fpath = OUTPUT_PATH / "groundfuelheight.bin"
         with open(output_fpath, "rb") as f:
@@ -275,15 +281,15 @@ def _generic_drawfire_slice_test(self, test_name, dims):
             # Read in each slice of the output data and compare to the
             # equivalent slice of the drawfire data
             for k in range(dims[2]):
-                output_data = self._process_gridded_bin_slice(f, dims[0],
-                                                              dims[1])
+                output_data = self._process_gridded_bin_slice(f, dims[0], dims[1])
                 self._assert_data_equality(drawfire_data[..., k], output_data)
 
             f.close()
 
     def test_fire_energy_to_atmos(self):
-        self._generic_drawfire_slice_test("fire-energy_to_atmos-",
-                                          (self.ny, self.nx, self.nz))
+        self._generic_drawfire_slice_test(
+            "fire-energy_to_atmos-", (self.ny, self.nx, self.nz)
+        )
 
     def test_windu(self):
         self._generic_drawfire_slice_test("windu", (self.ny, self.nx, self.nz))
@@ -324,12 +330,14 @@ def _assert_data_equality(self, data1, data2):
     def test_groundfuelheight(self):
         fuel_depth = self._load_fastfuels_data()
         input_data = self._load_and_process_input(
-            SIMULATION_PATH / "treesfueldepth.dat")
+            SIMULATION_PATH / "treesfueldepth.dat"
+        )
 
         self._assert_data_equality(fuel_depth, input_data)
 
         output_data = self._process_gridded_bin(
-            OUTPUT_PATH / "groundfuelheight.bin", (self.ny, self.nx))
+            OUTPUT_PATH / "groundfuelheight.bin", (self.ny, self.nx)
+        )
 
         fuel_depth[0, :] = 0
         fuel_depth[-1, :] = 0
@@ -341,12 +349,14 @@ def test_groundfuelheight(self):
     def test_total_initial_fuel_height(self):
         fuel_depth = self._load_fastfuels_data()
         input_data = self._load_and_process_input(
-            SIMULATION_PATH / "treesfueldepth.dat")
+            SIMULATION_PATH / "treesfueldepth.dat"
+        )
 
         self._assert_data_equality(fuel_depth, input_data)
 
         output_data = self._process_gridded_bin(
-            OUTPUT_PATH / "totalinitialfuelheight.bin", (self.ny, self.nx))
+            OUTPUT_PATH / "totalinitialfuelheight.bin", (self.ny, self.nx)
+        )
 
         fuel_depth[0, :] = 0
         fuel_depth[-1, :] = 0
@@ -356,14 +366,15 @@ def test_total_initial_fuel_height(self):
         self._assert_data_equality(output_data, fuel_depth)
 
     def _generic_drawfire_test(self, test_name, dims, *args):
-
         for t in (0, 300):
             drawfire_data = self._load_drawfire_data(
-                DRAWFIRE_PATH / f"{test_name}_{t}.npy")
+                DRAWFIRE_PATH / f"{test_name}_{t}.npy"
+            )
             if drawfire_data.shape[-1] != dims[-1]:
-                drawfire_data = drawfire_data[..., :dims[2]]
+                drawfire_data = drawfire_data[..., : dims[2]]
             output_data = self._process_gridded_bin(
-                OUTPUT_PATH / f"{test_name}00{t:03}.bin", dims)
+                OUTPUT_PATH / f"{test_name}00{t:03}.bin", dims
+            )
 
             self._assert_data_equality(drawfire_data, output_data)
 
@@ -371,8 +382,9 @@ def test_mburnt_integ(self):
         self._generic_drawfire_test("mburnt_integ-", (self.ny, self.nx))
 
     def test_fire_energy_to_atmos(self):
-        self._generic_drawfire_test("fire-energy_to_atmos-",
-                                    (self.ny, self.nx, self.nz))
+        self._generic_drawfire_test(
+            "fire-energy_to_atmos-", (self.ny, self.nx, self.nz)
+        )
 
     def test_windu(self):
         self._generic_drawfire_test("windu", (self.ny, self.nx, self.nz + 1))
@@ -387,8 +399,7 @@ class TestProcessCompressedBin:
     fire_indexes = sut._fire_indexes
 
     def _process_compressed_bin(self, f):
-        return outputs._process_compressed_bin(
-            f, self.dims, self.fire_indexes)
+        return outputs._process_compressed_bin(f, self.dims, self.fire_indexes)
 
     def test_fuels_dens(self):
         for t in (0, 300):