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

Issue:
Add test modules to cicd (#24)



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index b58c978..58bf27d 100644
--- a/.flake8
+++ b/.flake8
@@ -4,5 +4,7 @@ ignore =
     E501,
     # Whitespace before ':'. flake8 is wrong when inside slice expressions.
     E203,
+    # Ambiguous variable names. Just following the QUIC-Fire naming convention for gridlists.
+    E741,
     # Line break before binary operator. flake8 is wrong and goes against PEP8.
     W503,
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index c0ec3a7..5ad85f9 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -12,5 +12,6 @@ jobs:
       - uses: actions/setup-python@v4
         with:
           python-version: 3.x
+          cache: pip
       - run: pip install mkdocs mkdocstrings mkdocstrings-python mkdocs-material
       - run: mkdocs gh-deploy --force --clean --verbose
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
index 4556b88..a44706e 100644
--- a/.github/workflows/pre-commit.yaml
+++ b/.github/workflows/pre-commit.yaml
@@ -1,16 +1,15 @@
 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
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
         with:
-          python-version: "3.10"
+          python-version: "3.x"
+          cache: pip
       - uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3eb9745..d33ad8c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: Upload Python Package to PyPi on Release
+name: Release Workflow
 
 on:
   release:
@@ -8,17 +8,70 @@ permissions:
   contents: read
 
 jobs:
-  deploy:
+  upload-to-test-pypi:
+    runs-on: ubuntu-latest
 
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.x'
+        cache: pip
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install setuptools wheel twine
+    - name: Build package
+      run: |
+        python setup.py sdist bdist_wheel
+    - name: Upload to Test PyPi
+      env:
+        TWINE_USERNAME: __token__
+        TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
+      run: |
+        twine upload --repository testpypi dist/*
+
+  test-on-test-pypi:
+    needs: upload-to-test-pypi
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install package from Test PyPi
+        run: |
+          python -m pip install --upgrade pip
+          pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple quicfire-tools
+      - name: Install test dependencies
+        run: |
+          pip install -r requirements/test_requirements.txt
+      - name: Run pytest
+        env:
+          TEST_ENV: test_pypi
+        run: |
+          pytest
+
+  upload-to-pypi:
+    needs: test-on-test-pypi
     runs-on: ubuntu-latest
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
         python-version: '3.x'
+        cache: pip
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -30,3 +83,30 @@ jobs:
       run: |
         python setup.py sdist bdist_wheel
         twine upload dist/*
+
+  test-on-pypi:
+    needs: upload-to-pypi
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install package from PyPi
+        run: |
+          python -m pip install --upgrade pip
+          pip install quicfire-tools
+      - name: Install test dependencies
+        run: |
+          pip install -r requirements/test_requirements.txt
+      - name: Run pytest
+        env:
+          TEST_ENV: pypi
+        run: |
+          pytest
diff --git a/quicfire_tools/data/documentation/QU_Simparams.inp.json b/quicfire_tools/data/documentation/QU_simparams.inp.json
similarity index 100%
rename from quicfire_tools/data/documentation/QU_Simparams.inp.json
rename to quicfire_tools/data/documentation/QU_simparams.inp.json
diff --git a/quicfire_tools/data/templates/v5.1.1/sensor1.inp b/quicfire_tools/data/templates/v5.1.1/sensor1.inp
deleted file mode 100644
index 7d72798..0000000
--- a/quicfire_tools/data/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/examples.py b/quicfire_tools/examples.py
index 14ac049..fcf6136 100644
--- a/quicfire_tools/examples.py
+++ b/quicfire_tools/examples.py
@@ -1,10 +1,11 @@
-from quicfire_tools.inputs import SimulationInputs, QUIC_fire
-from quicfire_tools.ignitions import RectangleIgnition
+from __future__ import annotations
+
+from quicfire_tools.inputs import SimulationInputs
 
 
 def create_line_fire() -> SimulationInputs:
     # Set up the basic simulation data
-    sim_inputs = SimulationInputs.setup_simple_simulation(
+    sim_inputs = SimulationInputs.create_simulation(
         nx=200,
         ny=200,
         simulation_time=600,
@@ -12,32 +13,40 @@ def create_line_fire() -> SimulationInputs:
         wind_direction=90,
     )
 
-    # Make modifications to the QUIC_Fire input file
-    quic_fire: QUIC_fire = sim_inputs.get_input("QUIC_fire")
-    quic_fire.random_seed = 222
-    quic_fire.time_now = 1653321600
-    quic_fire.sim_time = 300
-    quic_fire.out_time_fire = 50
-    quic_fire.out_time_wind = 50
-    quic_fire.out_time_emis_rad = 50
-    quic_fire.out_time_wind_avg = 50
-    quic_fire.nz = 1
-    quic_fire.dz = 1.0
-    quic_fire.fuel_flag = 1
-    quic_fire.fuel_density = 0.7
-    quic_fire.fuel_moisture = 0.05
-    quic_fire.fuel_height = 1.0
-    line_ignition = RectangleIgnition(
+    # Set uniform fuel values
+    sim_inputs.set_uniform_fuels(
+        fuel_density=0.7,
+        fuel_moisture=0.05,
+        fuel_height=1.0,
+    )
+
+    # Set a line ignitions
+    sim_inputs.set_rectangle_ignition(
         x_min=150,
         y_min=100,
-        x_length=100,
+        x_length=10,
         y_length=100,
     )
-    quic_fire.ignition_type = line_ignition
-    quic_fire.ignitions_per_cell = 100
+
+    # Select which binary files to output
+    sim_inputs.set_output_files(
+        fuel_dens=True,
+        pm_emissions=True,
+        radiation=True,
+    )
+
+    # Make modifications to the QUIC_Fire input file
+    sim_inputs.quic_fire.random_seed = 222
+    sim_inputs.quic_fire.time_now = 1653321600
+    sim_inputs.quic_fire.sim_time = 300
+    sim_inputs.quic_fire.out_time_fire = 50
+    sim_inputs.quic_fire.out_time_wind = 50
+    sim_inputs.quic_fire.out_time_emis_rad = 50
+    sim_inputs.quic_fire.out_time_wind_avg = 50
+    sim_inputs.quic_fire.ignitions_per_cell = 100
 
     # Write the input file deck to a directory
-    sim_inputs.write_inputs("tmp")
+    sim_inputs.write_inputs("/tmp")
 
 
 line_fire_simulation = create_line_fire()
diff --git a/quicfire_tools/ignitions.py b/quicfire_tools/ignitions.py
index 5f214de..3172cc0 100644
--- a/quicfire_tools/ignitions.py
+++ b/quicfire_tools/ignitions.py
@@ -7,7 +7,7 @@
 from enum import Enum
 
 # External Imports
-from pydantic import BaseModel, SerializeAsAny
+from pydantic import BaseModel
 
 
 class IgnitionSources(int, Enum):
@@ -22,7 +22,7 @@ class IgnitionType(BaseModel):
     Test docs
     """
 
-    ignition_flag: SerializeAsAny[IgnitionSources]
+    ignition_flag: IgnitionSources
 
     def __str__(self):
         return (
@@ -50,7 +50,7 @@ class RectangleIgnition(IgnitionType):
         Length in the y-direction [m]
     """
 
-    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(1)
+    ignition_flag: IgnitionSources = IgnitionSources(1)
     x_min: int
     y_min: int
     x_length: int
@@ -87,7 +87,7 @@ class SquareRingIgnition(IgnitionType):
         Width in the y-direction [m]
     """
 
-    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(2)
+    ignition_flag: IgnitionSources = IgnitionSources(2)
     x_min: int
     y_min: int
     x_length: int
@@ -126,7 +126,7 @@ class CircularRingIgnition(IgnitionType):
         Width of the ring [m]
     """
 
-    ignition_flag: SerializeAsAny[IgnitionSources] = IgnitionSources(3)
+    ignition_flag: IgnitionSources = IgnitionSources(3)
     x_min: int
     y_min: int
     x_length: int
@@ -168,4 +168,6 @@ def default_line_ignition(nx, ny, wind_direction):
         x_length = 0.8 * (nx * 2)
         y_length = width
 
-    return x_min, y_min, x_length, y_length
+    return RectangleIgnition(
+        x_min=x_min, y_min=y_min, x_length=x_length, y_length=y_length
+    )
diff --git a/quicfire_tools/inputs.py b/quicfire_tools/inputs.py
index 595881e..83d252d 100644
--- a/quicfire_tools/inputs.py
+++ b/quicfire_tools/inputs.py
@@ -3,14 +3,13 @@
 """
 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
+from typing import Literal, Union
 
 # External Imports
 import numpy as np
@@ -66,46 +65,270 @@ class SimulationInputs:
     This is the fundamental class in the quicfire_tools.data 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.
-    """
 
-    _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}")
+    Attributes
+    ----------
+    rasterorigin: RasterOrigin
+        Object representing the rasterorigin.txt file.
+    qu_buildings: QU_Buildings
+        Object representing the QU_buildings.inp file.
+    qu_fileoptions: QU_Fileoptions
+        Object representing the QU_fileoptions.inp file.
+    qfire_advanced_user_inputs: QFire_Advanced_User_Inputs
+        Object representing the qfire_advanced_user_inputs.inp file.
+    qfire_bldg_advanced_user_inputs: QFire_Bldg_Advanced_User_Inputs
+        Object representing the qfire_bldg_advanced_user_inputs.inp file.
+    qfire_plume_advanced_user_inputs: QFire_Plume_Advanced_User_Inputs
+        Object representing the qfire_plume_advanced_user_inputs.inp file.
+    runtime_advanced_user_inputs: RuntimeAdvancedUserInputs
+        Object representing the runtime_advanced_user_inputs.inp file.
+    qu_movingcoords: QU_movingcoords
+        Object representing the QU_movingcoords.inp file.
+    qp_buildout: QP_buildout
+        Object representing the qp_buildout.inp file.
+    qu_metparams: QU_metparams
+        Object representing the QU_metparams.inp file.
+    quic_fire: QUIC_fire
+        Object representing the QUIC_fire.inp file.
+    gridlist: Gridlist
+        Object representing the gridlist.txt file.
+    sensor1: Sensor1
+        Object representing the sensor1.inp file.
+    qu_topoinputs: QU_TopoInputs
+        Object representing the QU_topoinputs.inp file.
+    qu_simparams: QU_Simparams
+        Object representing the QU_simparams.inp file.
+    """
 
-        # Initialize a dictionary of input files
-        self._inputs = {i.name: i for i in input_files}
+    def __init__(
+        self,
+        rasterorigin: RasterOrigin,
+        qu_buildings: QU_Buildings,
+        qu_fileoptions: QU_Fileoptions,
+        qfire_advanced_user_inputs: QFire_Advanced_User_Inputs,
+        qfire_bldg_advanced_user_inputs: QFire_Bldg_Advanced_User_Inputs,
+        qfire_plume_advanced_user_inputs: QFire_Plume_Advanced_User_Inputs,
+        runtime_advanced_user_inputs: RuntimeAdvancedUserInputs,
+        qu_movingcoords: QU_movingcoords,
+        qp_buildout: QP_buildout,
+        qu_metparams: QU_metparams,
+        quic_fire: QUIC_fire,
+        gridlist: Gridlist,
+        sensor1: Sensor1,
+        qu_topoinputs: QU_TopoInputs,
+        qu_simparams: QU_Simparams,
+    ):
+        # Store the input files as attributes
+        self.rasterorigin = rasterorigin
+        self.qu_buildings = qu_buildings
+        self.qu_fileoptions = qu_fileoptions
+        self.qfire_advanced_user_inputs = qfire_advanced_user_inputs
+        self.qfire_bldg_advanced_user_inputs = qfire_bldg_advanced_user_inputs
+        self.qfire_plume_advanced_user_inputs = qfire_plume_advanced_user_inputs
+        self.runtime_advanced_user_inputs = runtime_advanced_user_inputs
+        self.qu_movingcoords = qu_movingcoords
+        self.qp_buildout = qp_buildout
+        self.qu_metparams = qu_metparams
+        self.quic_fire = quic_fire
+        self.gridlist = gridlist
+        self.sensor1 = sensor1
+        self.qu_topoinputs = qu_topoinputs
+        self.qu_simparams = qu_simparams
+
+        # Create a dictionary from the local variables
+        self._input_files_dict = {
+            "rasterorigin": rasterorigin,
+            "qu_buildings": qu_buildings,
+            "qu_fileoptions": qu_fileoptions,
+            "qfire_advanced_user_inputs": qfire_advanced_user_inputs,
+            "qfire_bldg_advanced_user_inputs": qfire_bldg_advanced_user_inputs,
+            "qfire_plume_advanced_user_inputs": qfire_plume_advanced_user_inputs,
+            "runtime_advanced_user_inputs": runtime_advanced_user_inputs,
+            "qu_movingcoords": qu_movingcoords,
+            "qp_buildout": qp_buildout,
+            "qu_metparams": qu_metparams,
+            "quic_fire": quic_fire,
+            "gridlist": gridlist,
+            "sensor1": sensor1,
+            "qu_topoinputs": qu_topoinputs,
+            "qu_simparams": qu_simparams,
+        }
 
-    def list_inputs(self) -> list[str]:
+    @classmethod
+    def create_simulation(
+        cls,
+        nx: int,
+        ny: int,
+        fire_nz: int,
+        wind_speed: float,
+        wind_direction: int,
+        simulation_time: int,
+    ):
         """
-        List all input files by name in the SimulationInputs object.
+        Creates a SimulationInputs object to build a QUIC-Fire input file deck
+        and run a simulation.
+
+        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 [-]
+        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]
 
         Returns
         -------
-        list[str]
-            List of input file names.
+        SimulationInputs
+            Class containing the data to build a QUIC-Fire
+            input file deck and run a simulation using default parameters.
         """
-        return list(self._inputs.keys())
+        # Initialize default input files
+        rasterorigin = RasterOrigin()
+        qu_buildings = QU_Buildings()
+        qu_fileoptions = QU_Fileoptions()
+        qfire_advanced_user_inputs = QFire_Advanced_User_Inputs()
+        qfire_bldg_advanced_user_inputs = QFire_Bldg_Advanced_User_Inputs()
+        qfire_plume_advanced_user_inputs = QFire_Plume_Advanced_User_Inputs()
+        runtime_advanced_user_inputs = RuntimeAdvancedUserInputs()
+        qu_movingcoords = QU_movingcoords()
+        qp_buildout = QP_buildout()
+        qu_metparams = QU_metparams()
 
-    def get_input(self, input_name: str) -> InputFile:
+        # Initialize input files with required parameters
+        start_time = int(time.time())
+        ignition_type = default_line_ignition(nx, ny, wind_direction)
+        quic_fire = QUIC_fire(
+            nz=fire_nz,
+            time_now=start_time,
+            sim_time=simulation_time,
+            ignition_type=ignition_type,
+        )
+        gridlist = Gridlist(n=nx, m=ny, l=fire_nz)
+        sensor1 = Sensor1(
+            time_now=start_time, wind_speed=wind_speed, wind_direction=wind_direction
+        )
+        qu_topoinputs = QU_TopoInputs()
+        qu_simparams = QU_Simparams(nx=nx, ny=ny, wind_times=[start_time])
+
+        # Create the SimulationInputs object
+        return cls(
+            rasterorigin=rasterorigin,
+            qu_buildings=qu_buildings,
+            qu_fileoptions=qu_fileoptions,
+            qfire_advanced_user_inputs=qfire_advanced_user_inputs,
+            qfire_bldg_advanced_user_inputs=qfire_bldg_advanced_user_inputs,
+            qfire_plume_advanced_user_inputs=qfire_plume_advanced_user_inputs,
+            runtime_advanced_user_inputs=runtime_advanced_user_inputs,
+            qu_movingcoords=qu_movingcoords,
+            qp_buildout=qp_buildout,
+            qu_metparams=qu_metparams,
+            quic_fire=quic_fire,
+            gridlist=gridlist,
+            sensor1=sensor1,
+            qu_topoinputs=qu_topoinputs,
+            qu_simparams=qu_simparams,
+        )
+
+    @classmethod
+    def from_directory(cls, directory: str | Path) -> SimulationInputs:
         """
-        Retrieve an input file object by name from the SimulationInputs object.
+        Initializes a SimulationInputs object from a directory containing a
+        QUIC-Fire input file deck.
 
         Parameters
         ----------
-        input_name: str
-            Name of the input file to retrieve.
+        directory: str | Path
+            Directory containing a QUIC-Fire input file deck.
 
         Returns
         -------
-        InputFile
-            Input file object.
+        SimulationInputs
+            Class containing the input files in the QUIC-Fire input file deck.
         """
-        return self._inputs[input_name]
+        if isinstance(directory, str):
+            directory = Path(directory)
+        return cls(
+            rasterorigin=RasterOrigin.from_file(directory),
+            qu_buildings=QU_Buildings.from_file(directory),
+            qu_fileoptions=QU_Fileoptions.from_file(directory),
+            qfire_advanced_user_inputs=QFire_Advanced_User_Inputs.from_file(directory),
+            qfire_bldg_advanced_user_inputs=QFire_Bldg_Advanced_User_Inputs.from_file(
+                directory
+            ),
+            qfire_plume_advanced_user_inputs=QFire_Plume_Advanced_User_Inputs.from_file(
+                directory
+            ),
+            runtime_advanced_user_inputs=RuntimeAdvancedUserInputs.from_file(directory),
+            qu_movingcoords=QU_movingcoords.from_file(directory),
+            qp_buildout=QP_buildout.from_file(directory),
+            qu_metparams=QU_metparams.from_file(directory),
+            quic_fire=QUIC_fire.from_file(directory),
+            gridlist=Gridlist.from_file(directory),
+            sensor1=Sensor1.from_file(directory),
+            qu_topoinputs=QU_TopoInputs.from_file(directory),
+            qu_simparams=QU_Simparams.from_file(directory),
+        )
+
+    @classmethod
+    def from_dict(cls, data: dict) -> SimulationInputs:
+        """
+        Initializes a SimulationInputs object from a dictionary containing
+        input file data.
+
+        Parameters
+        ----------
+        data: dict
+            Dictionary containing input file data.
+        """
+        return cls(
+            rasterorigin=RasterOrigin.from_dict(data["rasterorigin"]),
+            qu_buildings=QU_Buildings.from_dict(data["qu_buildings"]),
+            qu_fileoptions=QU_Fileoptions.from_dict(data["qu_fileoptions"]),
+            qfire_advanced_user_inputs=QFire_Advanced_User_Inputs.from_dict(
+                data["qfire_advanced_user_inputs"]
+            ),
+            qfire_bldg_advanced_user_inputs=QFire_Bldg_Advanced_User_Inputs.from_dict(
+                data["qfire_bldg_advanced_user_inputs"]
+            ),
+            qfire_plume_advanced_user_inputs=QFire_Plume_Advanced_User_Inputs.from_dict(
+                data["qfire_plume_advanced_user_inputs"]
+            ),
+            runtime_advanced_user_inputs=RuntimeAdvancedUserInputs.from_dict(
+                data["runtime_advanced_user_inputs"]
+            ),
+            qu_movingcoords=QU_movingcoords.from_dict(data["qu_movingcoords"]),
+            qp_buildout=QP_buildout.from_dict(data["qp_buildout"]),
+            qu_metparams=QU_metparams.from_dict(data["qu_metparams"]),
+            quic_fire=QUIC_fire.from_dict(data["quic_fire"]),
+            gridlist=Gridlist.from_dict(data["gridlist"]),
+            sensor1=Sensor1.from_dict(data["sensor1"]),
+            qu_topoinputs=QU_TopoInputs.from_dict(data["qu_topoinputs"]),
+            qu_simparams=QU_Simparams.from_dict(data["qu_simparams"]),
+        )
+
+    @classmethod
+    def from_json(cls, path: str | Path) -> SimulationInputs:
+        """
+        Initializes a SimulationInputs object from a JSON file.
+
+        Parameters
+        ----------
+        path: str | Path
+            Path to the JSON file.
+        """
+        if isinstance(path, str):
+            path = Path(path)
+        with open(path, "r") as f:
+            data = json.load(f)
+        return cls.from_dict(data)
 
     def write_inputs(self, directory: str | Path, version: str = "latest") -> None:
         """
@@ -129,17 +352,15 @@ def write_inputs(self, directory: str | Path, version: str = "latest") -> None:
         if not directory.exists():
             raise NotADirectoryError(f"{directory} does not exist")
 
+        self._update_shared_attributes()
+
         # Skip writing gridlist and rasterorigin if fuel_flag == 1
         skip_inputs = []
-        quic_fire: QUIC_fire = self.get_input("QUIC_fire")
-        if quic_fire.fuel_flag == 1:
+        if self.quic_fire.fuel_flag == 1:
             skip_inputs.extend(["gridlist", "rasterorigin"])
 
         # Write each input file to the output directory
-        for input_name in self.list_inputs():
-            if input_name in skip_inputs:
-                continue
-            input_file = self.get_input(input_name)
+        for input_file in self._input_files_dict.values():
             input_file.to_file(directory, version=version)
 
         # Copy QU_landuse from the template directory to the output directory
@@ -149,433 +370,129 @@ def write_inputs(self, directory: str | Path, version: str = "latest") -> None:
             with open(output_file_path, "wb") as fout:
                 fout.write(ftemp.read())
 
-    @classmethod
-    def setup_simulation(
-        cls,
-        nx: int,
-        ny: int,
-        fire_nz: int,
-        simulation_time: int,
-        wind_speed: float,
-        wind_direction: float,
-        fuel_flag: int,
-        ignition_type: IgnitionType,
-        topo_type: TopoType,
-        fuel_density: float = None,
-        fuel_moisture: float = None,
-        fuel_height: float = None,
-        quic_nz: int = 22,
-        quic_height: float = 300,
-        dx: float = 2,
-        dy: float = 2,
-        fire_dz: float = 1,
-        output_time: int = 30,
-    ):
+    def to_dict(self) -> dict:
         """
-        Creates a SimulationInputs object to build a QUIC-Fire input file deck
-        and run a simulation.
+        Convert the object to a dictionary representation. The SimulationInputs
+        object is represented as a nest dictionary, with the name of each
+        input file as a key to that input file's dictionary representation.
 
-        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 [-]
-        simulation_time: int
-            Number of seconds to run the simulation for [s]
-        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).
-        ignition_type: IgnitionType
-            Ignition type specified as an IgnitionsType class from ignitions.py
-            1 = rectangle
-            2 = square ring
-            3 = circular ring
-            6 = ignite.dat (Firetec file)
-        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)
-        fuel_flag: int
-            Flag defning fuel input source [-]
-        quic_nz: int
-            Number of cells in the z-direction for the QUIC grid [-]
-        quic_height: float
-            Height of the QUIC grid [m]. Determines the cell size in the z-direction
-            for the QUIC grid. Must be 3 * the height of maximum elevation and tallest fuels.
-            See utils.calculate_quic_height
-        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]
-        output_time: int
-            Number of seconds between output files [s]
-        fuel_density: float
-            Fuel density for uniform fuels [kg/m**3]
-        fuel_moisture: float
-            Fuel moisture for uniform fuels [-]
-        fuel_height: float
-            Surface fuel height for uniform fuels [m]
-
-        Returns
-        -------
-        SimulationInputs
-            Class containing the data to build a QUIC-Fire
-            input file deck and run a simulation.
+        Returns:
+        --------
+        dict
+            Dictionary representation of the object.
         """
-        return cls(
-            cls.initialize_inputs(
-                nx,
-                ny,
-                fire_nz,
-                quic_nz,
-                quic_height,
-                dx,
-                dy,
-                fire_dz,
-                wind_speed,
-                wind_direction,
-                simulation_time,
-                output_time,
-                ignition_type,
-                topo_type,
-                fuel_flag,
-                fuel_density,
-                fuel_moisture,
-                fuel_height,
-            )
-        )
+        return {key: value.to_dict() for key, value in self._input_files_dict.items()}
 
-    @classmethod
-    def setup_simple_simulation(
-        cls,
-        nx: int,
-        ny: int,
-        simulation_time: int,
-        wind_speed: float,
-        wind_direction: float,
-        fire_nz: int = 1,
-        quic_nz: int = 22,
-        quic_height: float = 300,
-        dx: float = 2,
-        dy: float = 2,
-        fire_dz: float = 1,
-        output_time: int = 30,
-        topo_flag: int = 0,
-        fuel_flag: int = 1,
-        fuel_density: float = 0.6,
-        fuel_moisture: float = 0.5,
-        fuel_height: float = 1,
-    ):
+    def to_json(self, path: str | Path):
         """
-        Creates a SimulationInputs object with default data to
-        build a QUIC-Fire input file deck and run a simulation
-        with flat terrain and uniform fuels and a line ignition.
+        Write the object to a JSON file.
 
         Parameters
         ----------
-        nx: int
-            Number of cells in the x-direction [-]
-        ny: int
-            Number of cells in the y-direction [-]
-        simulation_time: int
-            Number of seconds to run the simulation for [s]
-        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).
-        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 [-]
-        quic_height: float
-            Height of the QUIC grid [m]. Determines the cell size in the z-direction
-            for the QUIC grid. Must be 3 * the height of maximum elevation and tallest fuels.
-            See utils.calculate_quic_height
-        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]
-        output_time: int
-            Number of seconds between output files [s]
-        ignition_flag: int
-            Flag defining ignition type [-]. See ignitions.IgnitionSources
-        topo_flag: int
-            Flag defining the topography type [-]. See topography.TopoSources
-        fuel_flag: int
-            Flag defning fuel input source [-]
-        fuel_density: float
-            Fuel density for uniform fuels [kg/m**3]
-        fuel_moisture: float
-            Fuel moisture for uniform fuels [-]
-        fuel_height: float
-            Surface fuel height for uniform fuels [m]
-
-        Returns
-        -------
-        SimulationInputs
-            Class containing the default data to build a QUIC-Fire
-            input file deck and run a simulation on flat terrain with
-            uniform fuels and a line ignition.
+        path : str | Path
+            Path to write the JSON file to.
         """
-        x_min_ig, y_min_ig, x_length_ig, y_length_ig = default_line_ignition(
-            nx, ny, wind_direction
-        )
-        ignition_type = RectangleIgnition(
-            x_min=x_min_ig,
-            y_min=y_min_ig,
-            x_length=x_length_ig,
-            y_length=y_length_ig,
-        )
-
-        topo_type = TopoType(topo_flag=TopoSources(topo_flag))
-
-        return cls(
-            cls.initialize_inputs(
-                nx,
-                ny,
-                fire_nz,
-                quic_nz,
-                quic_height,
-                dx,
-                dy,
-                fire_dz,
-                wind_speed,
-                wind_direction,
-                simulation_time,
-                output_time,
-                ignition_type,
-                topo_type,
-                fuel_flag,
-                fuel_density,
-                fuel_moisture,
-                fuel_height,
-            )
-        )
-
-    @classmethod
-    def setup_custom_simulation(
-        cls,
-        nx: int,
-        ny: int,
-        fire_nz: int,
-        simulation_time: int,
-        wind_speed: float,
-        wind_direction: float,
-        quic_nz: int = 22,
-        quic_height: float = 300,
-        dx: float = 2,
-        dy: float = 2,
-        fire_dz: float = 1,
-        output_time: int = 30,
-        ignition_flag: int = 6,
-        topo_flag: int = 5,
-        fuel_flag: int = 4,
-        fuel_density: float = None,
-        fuel_moisture: float = None,
-        fuel_height: float = None,
+        if isinstance(path, str):
+            path = Path(path)
+        with open(path, "w") as f:
+            json.dump(self.to_dict(), f, indent=4)
+
+    def set_custom_simulation(
+        self,
+        fuel: bool = True,
+        ignition: bool = True,
+        topo: bool = True,
     ):
-        """
-        Creates a SimulationInputs object with default data to
-        build a QUIC-Fire input file deck and run a simulation on a
-        domain with custom fuels, topography, and ignitions.
-
-        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 [-]
-        simulation_time: int
-            Number of seconds to run the simulation for [s]
-        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).
-        quic_nz: int
-            Number of cells in the z-direction for the QUIC grid [-]
-        quic_height: float
-            Height of the QUIC grid [m]. Determines the cell size in the z-direction
-            for the QUIC grid. Must be 3 * the height of maximum elevation and tallest fuels.
-            See utils.calculate_quic_height
-        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]
-        output_time: int
-            Number of seconds between output files [s]
-        fuel_flag: int
-            Flag defning fuel input source [-]
-        fuel_density: float
-            Fuel density for uniform fuels [kg/m**3]
-        fuel_moisture: float
-            Fuel moisture for uniform fuels [-]
-        fuel_height: float
-            Surface fuel height for uniform fuels [m]
-
-        Returns
-        -------
-        SimulationInputs
-            Class containing the default data to build a QUIC-Fire
-            input file deck and run a simulation on a domain with custom
-            fuels, topography, and ignitions.
-        """
-        ignition_type = IgnitionType(ignition_flag=IgnitionSources(ignition_flag))
-        topo_type = TopoType(topo_flag=TopoSources(topo_flag))
-
-        return cls(
-            cls.initialize_inputs(
-                nx,
-                ny,
-                fire_nz,
-                quic_nz,
-                quic_height,
-                dx,
-                dy,
-                fire_dz,
-                wind_speed,
-                wind_direction,
-                simulation_time,
-                output_time,
-                ignition_type,
-                topo_type,
-                fuel_flag,
-                fuel_density,
-                fuel_moisture,
-                fuel_height,
+        if fuel:
+            self.quic_fire.fuel_flag = 3
+            self.quic_fire.fuel_density = None
+            self.quic_fire.fuel_moisture = None
+            self.quic_fire.fuel_height = None
+        if ignition:
+            self.quic_fire.ignition_type = IgnitionType(
+                ignition_flag=IgnitionSources(6)
             )
-        )
+        if topo:
+            self.qu_topoinputs.topo_type = TopoType(topo_flag=TopoSources(5))
 
-    def initialize_inputs(
-        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,
-        ignition_type: IgnitionType,
-        topo_type: TopoType,
-        fuel_flag: int,
+    def set_uniform_fuels(
+        self,
         fuel_density: float,
         fuel_moisture: float,
         fuel_height: float,
     ):
-        # 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()
-        runtime_inputs = RuntimeAdvancedUserInputs()
-        qu_movingcoords = QU_movingcoords()
-        qp_buildout = QP_buildout()
-        qu_metparams = QU_metparams()
+        self.quic_fire.fuel_flag = 1
+        self.quic_fire.fuel_density = fuel_density
+        self.quic_fire.fuel_moisture = fuel_moisture
+        self.quic_fire.fuel_height = fuel_height
 
-        # Initialize input files with required parameters
-        start_time = int(time.time())
-        quic_fire = QUIC_fire(
-            nz=fire_nz,
-            time_now=start_time,
-            sim_time=simulation_time,
-            ignition_type=ignition_type,
-            fuel_flag=fuel_flag,
-            fuel_density=fuel_density,
-            fuel_moisture=fuel_moisture,
-            fuel_height=fuel_height,
-            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
-        )
-        qu_topo = QU_TopoInputs(topo_type=topo_type)
-        qu_simparams = QU_Simparams(
-            nx=nx,
-            ny=ny,
-            nz=quic_nz,
-            dx=dx,
-            dy=dy,
-            quic_domain_height=quic_height,
+    def set_rectangle_ignition(
+        self, x_min: int, y_min: int, x_length: int, y_length: int
+    ):
+        ignition = RectangleIgnition(
+            x_min=x_min, y_min=y_min, x_length=x_length, y_length=y_length
         )
-
-        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 input_files
-
-    @classmethod
-    def from_directory(cls, directory: str | Path) -> SimulationInputs:
-        """
-        Initializes a SimulationInputs object from a directory containing a
-        QUIC-Fire input file deck.
-
-        Parameters
-        ----------
-        directory: str | Path
-            Directory containing a QUIC-Fire input file deck.
-
-        Returns
-        -------
-        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)
+        self.quic_fire.ignition_type = ignition
+
+    def set_output_files(
+        self,
+        eng_to_atm: bool = False,
+        react_rate: bool = False,
+        fuel_dens: bool = False,
+        qf_wind: bool = False,
+        qu_wind_inst: bool = False,
+        qu_wind_avg: bool = False,
+        fuel_moist: bool = False,
+        mass_burnt: bool = False,
+        co_emissions: bool = False,
+        pm_emissions: bool = False,
+        water_emissions: bool = False,
+        radiation: bool = False,
+        intensity: bool = False,
+    ):
+        self.quic_fire.eng_to_atm_out = int(eng_to_atm)
+        self.quic_fire.react_rate_out = int(react_rate)
+        self.quic_fire.fuel_dens_out = int(fuel_dens)
+        self.quic_fire.qf_wind_out = int(qf_wind)
+        self.quic_fire.qu_wind_inst_out = int(qu_wind_inst)
+        self.quic_fire.qu_wind_avg_out = int(qu_wind_avg)
+        self.quic_fire.fuel_moist_out = int(fuel_moist)
+        self.quic_fire.mass_burnt_out = int(mass_burnt)
+        self.quic_fire.radiation_out = int(radiation)
+        self.quic_fire.intensity_out = int(intensity)
+        if water_emissions:
+            self.quic_fire.emissions_out = 5
+        elif co_emissions and pm_emissions:
+            self.quic_fire.emissions_out = 3
+        elif pm_emissions:
+            self.quic_fire.emissions_out = 2
+        elif co_emissions:
+            self.quic_fire.emissions_out = 1
+        else:
+            self.quic_fire.emissions_out = 0
+
+    def _update_shared_attributes(self):
+        self.gridlist.n = self.qu_simparams.nx
+        self.gridlist.m = self.qu_simparams.ny
+        self.gridlist.l = self.quic_fire.nz
+        self.gridlist.dx = self.qu_simparams.dx
+        self.gridlist.dy = self.qu_simparams.dy
+        if (
+            not self.sensor1.time_now
+            == self.quic_fire.time_now
+            == self.qu_simparams.wind_times[0]
+        ):
+            # TODO: How to handle conflicts
+            print(
+                f"WARNING: fire start time must be the same for all input files.\n"
+                f"Times: \n"
+                f"\tQUIC_fire.inp: {self.quic_fire.time_now}\n"
+                f"\tQU_simparams.inp: {self.qu_simparams.wind_times[0]}\n"
+                f"\tsensor1.inp: {self.sensor1.time_now}\n"
+                f"Setting all values to {self.quic_fire.time_now}"
+            )
+            self.sensor1.time_now = self.quic_fire.time_now
+            self.qu_simparams.wind_times[0] = self.quic_fire.time_now
 
 
 class InputFile(BaseModel, validate_assignment=True):
@@ -633,7 +550,7 @@ def print_documentation(self, parameter: str = None):
             key = key.replace("_", " ").capitalize()
             print(f"- {key}: {value}")
 
-    def to_dict(self):
+    def to_dict(self, include_private: bool = False):
         """
         Convert the object to a dictionary, excluding attributes that start
         with an underscore.
@@ -643,9 +560,14 @@ def to_dict(self):
         dict
             Dictionary representation of the object.
         """
-        return self.model_dump(
+        all_fields = self.model_dump(
             exclude={"name", "_extension", "_filename", "param_info"}
         )
+        if include_private:
+            return all_fields
+        return {
+            key: value for key, value in all_fields.items() if not key.startswith("_")
+        }
 
     def to_file(self, directory: Path, version: str = "latest"):
         """
@@ -665,7 +587,7 @@ def to_file(self, directory: Path, version: str = "latest"):
         with open(template_file_path, "r") as ftemp:
             src = Template(ftemp.read())
 
-        result = src.substitute(self.to_dict())
+        result = src.substitute(self.to_dict(include_private=True))
 
         output_file_path = directory / self._filename
         with open(output_file_path, "w") as fout:
@@ -676,6 +598,9 @@ def from_dict(cls, data: dict):
         return cls(**data)
 
 
+# TODO: Unify class naming
+
+
 class Gridlist(InputFile):
     """
     Class representing the gridlist.txt file. This file contains the grid
@@ -704,10 +629,31 @@ class Gridlist(InputFile):
     n: PositiveInt
     m: PositiveInt
     l: PositiveInt
-    dx: PositiveFloat
-    dy: PositiveFloat
-    dz: PositiveFloat
-    aa1: PositiveFloat
+    dx: PositiveFloat = 2
+    dy: PositiveFloat = 2
+    dz: PositiveFloat = 1
+    aa1: PositiveFloat = 1.0
+
+    @classmethod
+    def from_file(cls, directory: str | Path):
+        """
+        Initializes a Gridlist object from a directory containing a
+        gridlist.txt file.
+        """
+        if isinstance(directory, str):
+            directory = Path(directory)
+        with open(directory / "gridlist", "r") as f:
+            lines = f.read()
+
+        return cls(
+            n=int(lines.split("n=")[1].split()[0]),
+            m=int(lines.split("m=")[1].split()[0]),
+            l=int(lines.split("l=")[1].split()[0]),
+            dx=float(lines.split("dx=")[1].split()[0]),
+            dy=float(lines.split("dy=")[1].split()[0]),
+            dz=float(lines.split("dz=")[1].split()[0]),
+            aa1=float(lines.split("aa1=")[1].split()[0]),
+        )
 
 
 class RasterOrigin(InputFile):
@@ -905,16 +851,16 @@ class QU_Simparams(InputFile):
     _extension: str = ".inp"
     nx: PositiveInt
     ny: PositiveInt
-    nz: PositiveInt
-    dx: PositiveFloat
-    dy: PositiveFloat
-    quic_domain_height: PositiveFloat
+    nz: PositiveInt = 22
+    dx: PositiveFloat = 2
+    dy: PositiveFloat = 2
+    quic_domain_height: PositiveFloat = 300
+    wind_times: list[int]
     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
@@ -1123,12 +1069,14 @@ def from_file(cls, directory: str | Path):
         if stretch_grid_flag == 0:
             surface_vertical_cell_size = float(lines[7].strip().split("!")[0])
             number_surface_cells = int(lines[8].strip().split("!")[0])
+            quic_domain_height = surface_vertical_cell_size * number_surface_cells
             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]))
+            quic_domain_height = round(sum(custom_dz_array), 2)
             current_line = 9 + nz
         elif stretch_grid_flag == 3:
             surface_vertical_cell_size = float(lines[7].strip().split("!")[0])
@@ -1136,6 +1084,7 @@ def from_file(cls, directory: str | Path):
             _ = lines[9].strip().split("!")[0]
             for i in range(10, 10 + nz):
                 _from_file_dz_array.append(float(lines[i].strip().split("!")[0]))
+            quic_domain_height = round(sum(_from_file_dz_array), 2)
             current_line = 10 + nz
         else:
             raise ValueError("stretch_grid_flag must be 0, 1, or 3.")
@@ -1172,6 +1121,7 @@ def from_file(cls, directory: str | Path):
             nz=nz,
             dx=dx,
             dy=dy,
+            quic_domain_height=quic_domain_height,
             surface_vertical_cell_size=surface_vertical_cell_size,
             number_surface_cells=number_surface_cells,
             stretch_grid_flag=stretch_grid_flag,
@@ -1416,13 +1366,13 @@ class QUIC_fire(InputFile):
     stretch_grid_flag: Literal[0, 1] = 0
     dz: PositiveInt = 1
     dz_array: list[PositiveFloat] = []
-    fuel_flag: Literal[1, 2, 3, 4] = 4
-    fuel_density: PositiveFloat | None = None
-    fuel_moisture: PositiveFloat | None = None
-    fuel_height: PositiveFloat | None = None
-    ignition_type: SerializeAsAny[IgnitionType] = IgnitionType(
-        ignition_flag=IgnitionSources(6)
-    )
+    fuel_flag: Literal[1, 2, 3, 4] = 1
+    fuel_density: Union[PositiveFloat, None] = 0.5
+    fuel_moisture: Union[PositiveFloat, None] = 0.1
+    fuel_height: Union[PositiveFloat, None] = 1.0
+    ignition_type: Union[
+        RectangleIgnition, SquareRingIgnition, CircularRingIgnition, IgnitionType
+    ]
     ignitions_per_cell: PositiveInt = 2
     firebrand_flag: Literal[0, 1] = 0
     auto_kill: Literal[0, 1] = 1
@@ -1620,6 +1570,9 @@ def from_file(cls, directory: str | Path):
             )
         elif ignition_flag == 6:
             ignition_type = IgnitionType(ignition_flag=6)
+        else:
+            ignition_type = IgnitionType(ignition_flag=ignition_flag)
+
         current_line += add
         ignitions_per_cell = int(lines[current_line].strip().split("!")[0])
         current_line += 1
@@ -2144,7 +2097,7 @@ class Sensor1(InputFile):
     name: str = "sensor1"
     _extension: str = ".inp"
     time_now: PositiveInt
-    sensor_height: PositiveFloat = 6.1  # 20ft
+    sensor_height: PositiveFloat = 6.1
     wind_speed: PositiveFloat
     wind_direction: NonNegativeInt = Field(lt=360)
 
@@ -2158,8 +2111,7 @@ def _wind_lines(self) -> str:
         """
         return (
             f"{self.time_now} !Begining of time step in Unix Epoch time\n"
-            f"1 !site boundary layer flag (1 = log, 2 = exp, 3 = urban "
-            f"canopy, 4 = discrete data points)\n"
+            f"1 !site boundary layer flag (1 = log, 2 = exp, 3 = urban 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"
@@ -2172,6 +2124,7 @@ def from_file(cls, directory: str | Path):
             directory = Path(directory)
         with open(directory / "sensor1.inp", "r") as f:
             lines = f.readlines()
+        print("\n".join(lines))
         return cls(
             time_now=int(lines[6].strip().split("!")[0]),
             sensor_height=float(lines[11].split(" ")[0]),
diff --git a/quicfire_tools/utils.py b/quicfire_tools/utils.py
index 1d24b11..486664a 100644
--- a/quicfire_tools/utils.py
+++ b/quicfire_tools/utils.py
@@ -1,6 +1,7 @@
 """
 Utility functions for quicfire-tools.
 """
+from __future__ import annotations
 
 from pathlib import Path
 import numpy as np
diff --git a/requirements/docs.txt b/requirements/docs.txt
deleted file mode 100644
index 4a30805..0000000
--- a/requirements/docs.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-mkdocstrings
-mkdocstrings-python
diff --git a/requirements/readme.md b/requirements/readme.md
index c08b860..e7f6c91 100644
--- a/requirements/readme.md
+++ b/requirements/readme.md
@@ -8,11 +8,6 @@ Requirements for PyPi package.
 pip list --format=freeze > requirements.txt
 ```
 
-## docs.txt
+## test_requirements.txt
 
-Requirements for building documentation.
-
-## tests-3*.yml
-
-Requirements for testing with different Python versions. Uses conda environment
-yaml files.
+Requirements for testing.
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 696113a..c3f71f5 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,4 +1,4 @@
 dask
-numpy
-pydantic
-zarr
+numpy<2
+pydantic>=2
+zarr>=2
diff --git a/requirements/requirements.yml b/requirements/requirements.yml
deleted file mode 100644
index 7a182ec..0000000
--- a/requirements/requirements.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: fastfuels-sdk
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python=3.10
-  - requests
-  - numpy
-  - zarr
-  - scipy
-  - pandas
-  - ca-certificates
-  - openssl
-  - dask
diff --git a/setup.py b/setup.py
index 153143d..e35d15a 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,6 @@ def get_version():
         "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",


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..2e99638
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,32 @@
+name: Run Tests
+
+on:
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  test:
+
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        python-version: ["3.9", "3.10", "3.11", "3.12"]
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+        cache: pip
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements/test_requirements.txt
+    - name: Run pytest
+      env:
+        TEST_ENV: local
+      run: |
+        pytest
diff --git a/requirements/test_requirements.txt b/requirements/test_requirements.txt
new file mode 100644
index 0000000..8a51563
--- /dev/null
+++ b/requirements/test_requirements.txt
@@ -0,0 +1,8 @@
+pytest
+requests
+numpy<2
+dask
+pydantic>=2
+scipy
+xarray
+zarr>=2
diff --git a/requirements/tests-310.yml b/requirements/tests-310.yml
deleted file mode 100644
index 462413e..0000000
--- a/requirements/tests-310.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: fastfuels-sdk-test
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python=3.10
-  - pytest
-  - ca-certificates
-  - openssl
-  - requests
-  - pandas
-  - certifi
-  - zarr
-  - numpy
-  - scipy
-  - dask
-  - xarray
diff --git a/requirements/tests-311.yml b/requirements/tests-311.yml
deleted file mode 100644
index 9740021..0000000
--- a/requirements/tests-311.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: fastfuels-sdk-test
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python=3.11
-  - pytest
-  - ca-certificates
-  - openssl
-  - requests
-  - pandas
-  - certifi
-  - zarr
-  - numpy
-  - scipy
-  - dask
-  - xarray
diff --git a/requirements/tests-38.yml b/requirements/tests-38.yml
deleted file mode 100644
index 6c289d1..0000000
--- a/requirements/tests-38.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: fastfuels-sdk-test
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python=3.8
-  - pytest
-  - ca-certificates
-  - openssl
-  - requests
-  - pandas
-  - certifi
-  - zarr
-  - numpy
-  - scipy
-  - dask
-  - xarray
diff --git a/requirements/tests-39.yml b/requirements/tests-39.yml
deleted file mode 100644
index 32fd0d3..0000000
--- a/requirements/tests-39.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: fastfuels-sdk-test
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - python=3.9
-  - pytest
-  - ca-certificates
-  - openssl
-  - requests
-  - pandas
-  - certifi
-  - zarr
-  - numpy
-  - scipy
-  - dask
-  - xarray
diff --git a/tests/__init__.py b/tests/__init__.py
index 7dcdbfc..c658b55 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,6 +1,6 @@
 import os
 
-TEST_ENV = os.environ.get("TEST_ENV", "local")
+TEST_ENV = os.environ.get("TEST_ENV", default="local")
 
 if TEST_ENV == "local":
     import sys
diff --git a/tests/data/test-inputs/fuel_lines.txt b/tests/data/test-inputs/fuel_lines.txt
new file mode 100644
index 0000000..c1bf86b
--- /dev/null
+++ b/tests/data/test-inputs/fuel_lines.txt
@@ -0,0 +1,6 @@
+1	! fuel density flag:  1 = uniform; 2 = provided thru QF_FuelMoisture.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)
+0.5
+1	! 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)
+1.0
+1	! fuel height flag:  1 = uniform; 2 = provided thru QF_FuelMoisture.inp, 3 = Firetech files for quic grid, 4 = Firetech files for different grid (need interpolation)
+0.75
\ No newline at end of file
diff --git a/tests/data/test-inputs/stretchgrid_3.txt b/tests/data/test-inputs/stretchgrid_3.txt
index b3f8e15..5ea4364 100644
--- a/tests/data/test-inputs/stretchgrid_3.txt
+++ b/tests/data/test-inputs/stretchgrid_3.txt
@@ -7,23 +7,23 @@
 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
+1.0780487579553308
+1.3121950318213225
+1.7024388215979753
+2.248780127285289
+2.9512189488832643
+3.8097552863919013
+4.824389139811199
+5.995120509141158
+7.321949394381777
+8.804875795533059
+10.443899712595
+12.239021145567603
+14.190240094450866
+16.297556559244796
+18.56097053994938
+20.980482036564624
+23.55609104909054
+26.287797577527108
+29.175601621874343
+32.21956785033345
\ No newline at end of file
diff --git a/tests/test_input_module.py b/tests/test_input_module.py
index 94ce691..75940db 100644
--- a/tests/test_input_module.py
+++ b/tests/test_input_module.py
@@ -1,6 +1,8 @@
 """
 Test module for the data module of the quicfire_tools package.
 """
+from __future__ import annotations
+
 import pytest
 from pydantic import ValidationError
 from pathlib import Path
@@ -23,11 +25,17 @@
     Sensor1,
     SimulationInputs,
 )
-from quicfire_tools.ignitions import RectangleIgnition
-from quicfire_tools.topography import TopoType, GaussianHillTopo
+from quicfire_tools.ignitions import (
+    IgnitionType,
+    IgnitionSources,
+    RectangleIgnition,
+    default_line_ignition,
+)
+from quicfire_tools.topography import TopoType, TopoSources, GaussianHillTopo
 
-# Create a tmp/ directory to store the temporary test files
-Path("tmp/").mkdir(exist_ok=True)
+TEST_DIR = Path(__file__).parent
+TMP_DIR = TEST_DIR / "tmp"
+TMP_DIR.mkdir(exist_ok=True)
 
 
 class TestGridList:
@@ -80,10 +88,10 @@ def test_to_docs(self):
     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/")
+        gridlist.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/gridlist", "r") as file:
+        with open(TMP_DIR / "gridlist", "r") as file:
             lines = file.readlines()
             assert lines[0].split("=")[1].strip() == "10"
             assert lines[1].split("=")[1].strip() == "10"
@@ -97,6 +105,14 @@ def test_to_file(self):
         with pytest.raises(FileNotFoundError):
             gridlist.to_file("/non_existent_path/gridlist.txt")
 
+    def test_from_file(self):
+        """Test initializing a class from a gridlist file."""
+        gridlist = Gridlist(n=10, m=10, l=10, dx=1.0, dy=1.0, dz=1.0, aa1=1.0)
+        gridlist.to_file(TMP_DIR)
+        test_object = Gridlist.from_file(TMP_DIR)
+        assert isinstance(test_object, Gridlist)
+        assert gridlist == test_object
+
 
 class TestRasterOrigin:
     def test_init(self):
@@ -155,10 +171,10 @@ def test_to_docs(self):
     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/")
+        raster_origin.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/rasterorigin.txt", "r") as file:
+        with open(TMP_DIR / "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
@@ -170,8 +186,8 @@ def test_to_file(self):
     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/")
+        raster_origin.to_file(TMP_DIR)
+        test_object = RasterOrigin.from_file(TMP_DIR)
         assert isinstance(test_object, RasterOrigin)
         assert raster_origin == test_object
 
@@ -251,10 +267,10 @@ def test_to_docs(self):
     def test_to_file(self):
         """Test the to_file method of a QU_Buildings object."""
         qu_buildings = QU_Buildings()
-        qu_buildings.to_file("tmp/")
+        qu_buildings.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QU_buildings.inp", "r") as file:
+        with open(TMP_DIR / "QU_buildings.inp", "r") as file:
             lines = file.readlines()
             assert (
                 float(lines[1].strip().split("\t")[0])
@@ -275,8 +291,8 @@ def test_to_file(self):
     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/")
+        qu_buildings.to_file(TMP_DIR)
+        test_object = QU_Buildings.from_file(TMP_DIR)
         assert isinstance(test_object, QU_Buildings)
         assert qu_buildings == test_object
 
@@ -356,10 +372,10 @@ def test_to_docs(self):
     def test_to_file(self):
         """Test the to_file method of a QU_Buildings object."""
         qu_fileoptions = QU_Fileoptions()
-        qu_fileoptions.to_file("tmp/")
+        qu_fileoptions.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QU_fileoptions.inp", "r") as file:
+        with open(TMP_DIR / "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
@@ -374,8 +390,8 @@ def test_to_file(self):
     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/")
+        qu_fileoptions.to_file(TMP_DIR)
+        test_object = QU_Fileoptions.from_file(TMP_DIR)
         assert isinstance(test_object, QU_Fileoptions)
         assert qu_fileoptions == test_object
 
@@ -383,7 +399,9 @@ def test_from_file(self):
 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)
+        return QU_Simparams(
+            nx=100, ny=100, nz=26, dx=2.0, dy=2, quic_domain_height=250, wind_times=[0]
+        )
 
     def test_init(self):
         # Test default initialization
@@ -411,18 +429,10 @@ def test_init(self):
 
         # Test data type casting
         qu_simparams = QU_Simparams(
-            nx="100", ny=100, nz=26, dx=2, dy=2, quic_domain_height=5
+            nx="100", ny=100, nz=26, dx=2, dy=2, quic_domain_height=5, wind_times=["0."]
         )
-        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
+        assert qu_simparams.wind_times == [0]
 
         # Test invalid stretch_grid_flags
         for invalid_flag in [-1, 4, "1", 1.0, 1.5, 2]:
@@ -452,7 +462,7 @@ 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-inputs/stretchgrid_0.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/stretchgrid_0.txt") as f:
             expected_lines = f.readlines()
         assert vertical_grid_lines == "".join(expected_lines)
 
@@ -478,14 +488,14 @@ def test_stretch_grid_flag_1(self):
         # 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-inputs/stretchgrid_1.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/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-inputs/stretchgrid_3.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/stretchgrid_3.txt") as f:
             expected_lines = f.readlines()
         assert vertical_grid_lines == "".join(expected_lines)
 
@@ -494,29 +504,29 @@ def test_generate_vertical_grid(self):
 
         # Test stretch_grid_flag = 0
         qu_simparams.stretch_grid_flag = 0
-        with open("data/test-inputs/stretchgrid_0.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/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-inputs/stretchgrid_1.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/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-inputs/stretchgrid_3.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/stretchgrid_3.txt") as f:
             expected_lines = f.readlines()
         assert qu_simparams._vertical_grid_lines == "".join(expected_lines)
 
-    def test_generate_wind_times(self):
+    def test_generate_wind_time_lines(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-inputs/wind_times.txt") as f:
+        with open(TEST_DIR / "data/test-inputs/wind_times.txt") as f:
             expected_lines = f.readlines()
         assert wind_times_lines == "".join(expected_lines)
 
@@ -551,7 +561,6 @@ def test_to_dict(self):
         )
         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
@@ -587,10 +596,18 @@ def test_to_docs(self):
         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"]:
+            if key in [
+                "_dz_array",
+                "_vertical_grid_lines",
+                "_wind_time_lines",
+                "custom_dz_array",
+                "quic_domain_height",
+            ]:
                 continue
             assert key in result_docs
         for key in result_docs:
+            if key in ["dz_array"]:
+                continue
             assert key in result_dict
 
     def test_to_file(self):
@@ -598,10 +615,10 @@ 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/")
+        qu_simparams.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QU_simparams.inp", "r") as file:
+        with open(TMP_DIR / "QU_simparams.inp", "r") as file:
             lines = file.readlines()
 
         # Check nx, ny, nz, dx, dy
@@ -700,27 +717,27 @@ def test_from_file(self):
         """
         # Test stretch grid flag = 3
         qu_simparams = self.get_test_object()
-        qu_simparams.to_file("tmp/")
-        test_object = QU_Simparams.from_file("tmp/")
+        qu_simparams.to_file(TMP_DIR)
+        test_object = QU_Simparams.from_file(TMP_DIR)
         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 = 0
+        # qu_simparams = self.get_test_object()
+        # qu_simparams.stretch_grid_flag = 0
+        # qu_simparams.to_file(TMP_DIR)
+        # test_object = QU_Simparams.from_file(TMP_DIR)
+        # 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
+        # # 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_DIR)
+        # test_object = QU_Simparams.from_file(TMP_DIR)
+        # assert isinstance(test_object, QU_Simparams)
+        # assert qu_simparams == test_object
 
 
 class TestQFire_Advanced_User_Inputs:
@@ -840,10 +857,10 @@ def test_to_file(self):
         qfire_advanced_user_inputs = QFire_Advanced_User_Inputs(
             fraction_cells_launch_firebrands=0.1
         )
-        qfire_advanced_user_inputs.to_file("tmp/")
+        qfire_advanced_user_inputs.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QFIRE_advanced_user_inputs.inp", "r") as file:
+        with open(TMP_DIR / "QFire_Advanced_User_Inputs.inp", "r") as file:
             lines = file.readlines()
             assert (
                 float(lines[0].strip().split("!")[0])
@@ -912,8 +929,8 @@ 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/")
+        qfire_advanced_user_inputs.to_file(TMP_DIR)
+        test_object = QFire_Advanced_User_Inputs.from_file(TMP_DIR)
         assert isinstance(test_object, QFire_Advanced_User_Inputs)
         assert qfire_advanced_user_inputs == test_object
 
@@ -921,7 +938,14 @@ def test_from_file(self):
 class TestQUIC_fire:
     @staticmethod
     def get_test_object():
-        return QUIC_fire(nz=26, sim_time=60, time_now=1695311421)
+        return QUIC_fire(
+            nz=26,
+            sim_time=60,
+            time_now=1695311421,
+            ignition_type=RectangleIgnition(
+                x_min=20, y_min=20, x_length=10, y_length=160
+            ),
+        )
 
     @staticmethod
     def get_complex_test_object():
@@ -951,8 +975,7 @@ def test_init(self):
         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)
+        quic_fire.nz = "26"
         assert quic_fire.nz == 26
 
         # Test stretch grid input
@@ -964,55 +987,17 @@ def test_init(self):
         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 data
-        quic_fire = QUIC_fire(nz=26, sim_time=60, time_now=1695311421)
-        assert quic_fire.fuel_density is None
-
-        # Change fuel flag
-        quic_fire.fuel_flag = 1
-
-        # Writing QUIC_fire to file at this point should throw an error because
-        # fuel_density, fuel_moisture, and fuel_height are not set
-        with pytest.raises(ValueError):
-            quic_fire.to_file("tmp/")
-
         # Set fuel_density, fuel_moisture, and fuel_height
         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"
-        )
+        with open(TEST_DIR / "data/test-inputs/fuel_lines.txt") as f:
+            expected_lines = f.readlines()
+        assert quic_fire._fuel_lines == "".join(expected_lines)
 
     def test_to_dict(self):
         """Test the to_dict method of a QUIC_fire object."""
@@ -1032,12 +1017,11 @@ def test_to_dict(self):
         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["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
@@ -1066,11 +1050,6 @@ def test_to_dict(self):
         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
-
     def test_from_dict(self):
         quic_fire = self.get_test_object()
         test_dict = quic_fire.to_dict()
@@ -1102,8 +1081,8 @@ def test_from_dict(self):
 
     def test_to_file(self):
         quic_fire = self.get_complex_test_object()
-        quic_fire.to_file("tmp/")
-        with open("tmp/QUIC_fire.inp", "r") as file:
+        quic_fire.to_file(TMP_DIR)
+        with open(TMP_DIR / "QUIC_fire.inp", "r") as file:
             lines = file.readlines()
 
         assert quic_fire.fire_flag == int(lines[0].strip().split("!")[0])
@@ -1198,8 +1177,8 @@ 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/")
+        quic_fire.to_file(TMP_DIR)
+        test_object = QUIC_fire.from_file(TMP_DIR)
         assert isinstance(test_object, QUIC_fire)
         assert quic_fire == test_object
 
@@ -1293,10 +1272,10 @@ def test_to_docs(self):
 
     def test_to_file(self):
         bldg_inputs = QFire_Bldg_Advanced_User_Inputs()
-        bldg_inputs.to_file("tmp/")
+        bldg_inputs.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QFIRE_bldg_advanced_user_inputs.inp", "r") as file:
+        with open(TMP_DIR / "QFire_Bldg_Advanced_User_Inputs.inp", "r") as file:
             lines = file.readlines()
         assert (
             int(lines[0].strip().split("!")[0])
@@ -1331,13 +1310,13 @@ def test_to_file(self):
         # Test writing to a non-existent directory
         with pytest.raises(FileNotFoundError):
             bldg_inputs.to_file(
-                "/non_existent_path/QFIRE_bldg_advanced_user_inputs.inp"
+                "/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/")
+        bldg_inputs.to_file(TMP_DIR)
+        test_object = QFire_Bldg_Advanced_User_Inputs.from_file(TMP_DIR)
         assert isinstance(test_object, QFire_Bldg_Advanced_User_Inputs)
         assert bldg_inputs == test_object
 
@@ -1465,10 +1444,10 @@ def test_to_docs(self):
 
     def test_to_file(self):
         plume_inputs = QFire_Plume_Advanced_User_Inputs()
-        plume_inputs.to_file("tmp/")
+        plume_inputs.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QFIRE_plume_advanced_user_inputs.inp", "r") as file:
+        with open(TMP_DIR / "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
@@ -1519,10 +1498,16 @@ def test_to_file(self):
             == plume_inputs.plume_to_grid_intersection_flag
         )
 
+        # Test writing to a non-existent directory
+        with pytest.raises(FileNotFoundError):
+            plume_inputs.to_file(
+                "/non_existent_path/QFire_Plume_Advanced_User_Inputs.inp"
+            )
+
     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/")
+        plume_inputs.to_file(TMP_DIR)
+        test_object = QFire_Plume_Advanced_User_Inputs.from_file(TMP_DIR)
         assert isinstance(test_object, QFire_Plume_Advanced_User_Inputs)
         assert plume_inputs == test_object
 
@@ -1600,8 +1585,8 @@ def test_from_dict(self):
 
     def test_to_file(self):
         topoinputs = self.get_default_test_object()
-        topoinputs.to_file("tmp/")
-        with open("tmp/QU_TopoInputs.inp", "r") as file:
+        topoinputs.to_file(TMP_DIR)
+        with open(TMP_DIR / "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
@@ -1641,8 +1626,8 @@ def test_to_file(self):
         )
 
         topoinputs = self.get_complex_test_object()
-        topoinputs.to_file("tmp/")
-        with open("tmp/QU_TopoInputs.inp", "r") as file:
+        topoinputs.to_file(TMP_DIR)
+        with open(TMP_DIR / "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
@@ -1689,14 +1674,14 @@ def test_to_file(self):
 
     def test_from_file(self):
         topoinputs = self.get_default_test_object()
-        topoinputs.to_file("tmp/")
-        test_object = QU_TopoInputs.from_file("tmp/")
+        topoinputs.to_file(TMP_DIR)
+        test_object = QU_TopoInputs.from_file(TMP_DIR)
         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/")
+        topoinputs.to_file(TMP_DIR)
+        test_object = QU_TopoInputs.from_file(TMP_DIR)
         assert isinstance(test_object, QU_TopoInputs)
         assert topoinputs == test_object
 
@@ -1713,8 +1698,8 @@ def test_init(self):
 
     def test_from_file(self):
         raui = self.get_test_object()
-        raui.to_file("tmp/")
-        test_object = RuntimeAdvancedUserInputs.from_file("tmp/")
+        raui.to_file(TMP_DIR)
+        test_object = RuntimeAdvancedUserInputs.from_file(TMP_DIR)
         assert raui == test_object
 
 
@@ -1728,8 +1713,8 @@ def test_init(self):
 
     def test_from_file(self):
         qu_moving = self.get_test_object()
-        qu_moving.to_file("tmp/")
-        test_object = QU_movingcoords.from_file("tmp/")
+        qu_moving.to_file(TMP_DIR)
+        test_object = QU_movingcoords.from_file(TMP_DIR)
         assert qu_moving == test_object
 
 
@@ -1743,8 +1728,8 @@ def test_init(self):
 
     def test_from_file(self):
         qp_buildout = self.get_test_object()
-        qp_buildout.to_file("tmp/")
-        test_object = QP_buildout.from_file("tmp/")
+        qp_buildout.to_file(TMP_DIR)
+        test_object = QP_buildout.from_file(TMP_DIR)
         assert qp_buildout == test_object
 
 
@@ -1771,18 +1756,18 @@ def test_from_dict(self):
 
     def test_to_file(self):
         qu_metparams = self.get_test_object()
-        qu_metparams.to_file("tmp/")
+        qu_metparams.to_file(TMP_DIR)
 
         # Read the content of the file and check for correctness
-        with open("tmp/QU_metparams.inp", "r") as file:
+        with open(TMP_DIR / "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/")
+        qu_metparams.to_file(TMP_DIR)
+        test_object = QU_metparams.from_file(TMP_DIR)
         assert isinstance(test_object, QU_metparams)
         assert qu_metparams == test_object
 
@@ -1803,98 +1788,220 @@ def test_error(self):
         with pytest.raises(ValidationError):
             sensor1.wind_direction = 360
 
+    def test_to_file(self):
+        sensor1 = self.get_test_object()
+        sensor1.to_file(TMP_DIR)
+
+        # Read the content of the file and check for correctness
+        with open(TMP_DIR / "sensor1.inp", "r") as file:
+            lines = file.readlines()
+        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])
+        assert time_now == sensor1.time_now
+        assert sensor_height == sensor1.sensor_height
+        assert wind_speed == sensor1.wind_speed
+        assert wind_direction == sensor1.wind_direction
+
     def test_from_file(self):
         sensor1 = self.get_test_object()
-        sensor1.to_file("tmp/")
-        test_object = Sensor1.from_file("tmp/")
+        sensor1.to_file(TMP_DIR)
+        test_object = Sensor1.from_file(TMP_DIR)
         assert sensor1 == test_object
 
 
 class TestSimulationInputs:
     @staticmethod
     def get_test_object():
-        topo = GaussianHillTopo(
-            x_hilltop=50, y_hilltop=50, elevation_max=100, elevation_std=20
-        )
-        ignite = RectangleIgnition(x_min=20, y_min=20, x_length=10, y_length=160)
-        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,
-            topo_type=topo,
-            ignition_type=ignite,
-            fuel_flag=1,
-            fuel_density=0.6,
-            fuel_moisture=0.5,
-            fuel_height=1.0,
+        return SimulationInputs.create_simulation(
+            nx=150,
+            ny=150,
+            fire_nz=1,
+            wind_speed=5.0,
+            wind_direction=90,
+            simulation_time=65,
         )
 
     @staticmethod
-    def get_simple_test_object():
-        return SimulationInputs.setup_simple_simulation(
-            nx=100,
-            ny=100,
-            simulation_time=600,
-            wind_speed=2.7,
-            wind_direction=270,
+    def compare_simulation_inputs(a: SimulationInputs, b: SimulationInputs):
+        for a_input_file, b_input_file in zip(
+            a._input_files_dict.values(), b._input_files_dict.values()
+        ):
+            assert a_input_file == b_input_file
+
+    def test_basic_inputs(self):
+        sim_inputs = self.get_test_object()
+        assert isinstance(sim_inputs, SimulationInputs)
+
+    def test_input_files(self):
+        sim_inputs = self.get_test_object()
+        assert isinstance(sim_inputs.gridlist, Gridlist)
+        assert isinstance(sim_inputs.rasterorigin, RasterOrigin)
+        assert isinstance(sim_inputs.qu_buildings, QU_Buildings)
+        assert isinstance(sim_inputs.qu_fileoptions, QU_Fileoptions)
+        assert isinstance(sim_inputs.qu_simparams, QU_Simparams)
+        assert isinstance(
+            sim_inputs.qfire_advanced_user_inputs, QFire_Advanced_User_Inputs
         )
+        assert isinstance(sim_inputs.quic_fire, QUIC_fire)
+        assert isinstance(
+            sim_inputs.qfire_bldg_advanced_user_inputs, QFire_Bldg_Advanced_User_Inputs
+        )
+        assert isinstance(
+            sim_inputs.qfire_plume_advanced_user_inputs,
+            QFire_Plume_Advanced_User_Inputs,
+        )
+        assert isinstance(sim_inputs.qu_topoinputs, QU_TopoInputs)
+        assert isinstance(
+            sim_inputs.runtime_advanced_user_inputs, RuntimeAdvancedUserInputs
+        )
+        assert isinstance(sim_inputs.qu_movingcoords, QU_movingcoords)
+        assert isinstance(sim_inputs.qp_buildout, QP_buildout)
+        assert isinstance(sim_inputs.qu_metparams, QU_metparams)
+        assert isinstance(sim_inputs.sensor1, Sensor1)
+
+        assert sim_inputs.quic_fire.nz == 1
+        assert sim_inputs.quic_fire.sim_time == 65
+        assert sim_inputs.qu_simparams.nx == 150
+        assert sim_inputs.qu_simparams.ny == 150
+        assert sim_inputs.quic_fire.time_now == sim_inputs.sensor1.time_now
+        assert sim_inputs.qu_simparams.wind_times[0] == sim_inputs.sensor1.time_now
+        assert sim_inputs.sensor1.wind_speed == 5.0
+        assert sim_inputs.sensor1.wind_direction == 90
+
+    def test_set_uniform_fuels(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.set_uniform_fuels(
+            fuel_density=0.6, fuel_moisture=0.05, fuel_height=0.9
+        )
+        assert sim_inputs.quic_fire.fuel_flag == 1
+        assert sim_inputs.quic_fire.fuel_density == 0.6
+        assert sim_inputs.quic_fire.fuel_moisture == 0.05
+        assert sim_inputs.quic_fire.fuel_height == 0.9
 
-    @staticmethod
-    def get_custom_test_object():
-        return SimulationInputs.setup_custom_simulation(
-            nx=100,
-            ny=100,
-            fire_nz=40,
-            simulation_time=600,
-            wind_speed=2.7,
-            wind_direction=270,
+    def test_set_rectangle_ignition(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.set_rectangle_ignition(
+            x_min=20,
+            y_min=20,
+            x_length=10,
+            y_length=110,
+        )
+        assert sim_inputs.quic_fire.ignition_type == RectangleIgnition(
+            x_min=20,
+            y_min=20,
+            x_length=10,
+            y_length=110,
         )
 
-    def test_basic_inputs(self):
+    def test_set_output_files(self):
         sim_inputs = self.get_test_object()
-        assert isinstance(sim_inputs, SimulationInputs)
+        sim_inputs.set_output_files(
+            mass_burnt=True, co_emissions=True, pm_emissions=True
+        )
+        assert sim_inputs.quic_fire.eng_to_atm_out == 0
+        assert sim_inputs.quic_fire.react_rate_out == 0
+        assert sim_inputs.quic_fire.fuel_dens_out == 0
+        assert sim_inputs.quic_fire.qf_wind_out == 0
+        assert sim_inputs.quic_fire.qu_wind_inst_out == 0
+        assert sim_inputs.quic_fire.qu_wind_avg_out == 0
+        assert sim_inputs.quic_fire.fuel_moist_out == 0
+        assert sim_inputs.quic_fire.mass_burnt_out == 1
+        assert sim_inputs.quic_fire.radiation_out == 0
+        assert sim_inputs.quic_fire.intensity_out == 0
+        assert sim_inputs.quic_fire.emissions_out == 3
+
+        sim_inputs.set_output_files(water_emissions=True)
+        assert sim_inputs.quic_fire.emissions_out == 5
+
+        sim_inputs.set_output_files(co_emissions=True, water_emissions=True)
+        assert sim_inputs.quic_fire.emissions_out == 5
+
+        sim_inputs.set_output_files(co_emissions=True)
+        assert sim_inputs.quic_fire.emissions_out == 1
+
+    def test_set_custom_simulation(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.set_custom_simulation()
+        assert sim_inputs.quic_fire.fuel_flag == 3
+        assert sim_inputs.quic_fire.fuel_density is None
+        assert sim_inputs.quic_fire.fuel_moisture is None
+        assert sim_inputs.quic_fire.fuel_height is None
+        assert sim_inputs.quic_fire.ignition_type == IgnitionType(
+            ignition_flag=IgnitionSources(6)
+        )
+        assert sim_inputs.qu_topoinputs.topo_type == TopoType(topo_flag=TopoSources(5))
 
-    def test_simple(self):
-        sim_inputs = self.get_simple_test_object()
-        assert isinstance(sim_inputs, SimulationInputs)
-        quic_fire = sim_inputs.get_input("QUIC_fire")
-        topo_inputs = sim_inputs.get_input("QU_TopoInputs")
-        assert quic_fire.fuel_flag == 1
-        assert topo_inputs.topo_type.topo_flag.value == 0
-        assert quic_fire.ignition_type.ignition_flag.value == 1
-
-    def test_custum(self):
-        sim_inputs = self.get_custom_test_object()
-        assert isinstance(sim_inputs, SimulationInputs)
-        quic_fire = sim_inputs.get_input("QUIC_fire")
-        topo_inputs = sim_inputs.get_input("QU_TopoInputs")
-        assert quic_fire.fuel_flag == 4
-        assert topo_inputs.topo_type.topo_flag.value == 5
-        assert quic_fire.ignition_type.ignition_flag.value == 6
+        sim_inputs = self.get_test_object()
+        sim_inputs.set_custom_simulation(topo=False)
+        assert sim_inputs.qu_topoinputs.topo_type == TopoType(topo_flag=TopoSources(0))
 
-    def test_list_inputs(self):
         sim_inputs = self.get_test_object()
-        inputs = sim_inputs.list_inputs()
-        assert "rasterorigin" in inputs
+        sim_inputs.set_custom_simulation(ignition=False)
+        assert sim_inputs.quic_fire.ignition_type == default_line_ignition(150, 150, 90)
 
-    def test_get_input(self):
         sim_inputs = self.get_test_object()
-        rasterorigin = sim_inputs.get_input("rasterorigin")
-        assert isinstance(rasterorigin, RasterOrigin)
+        sim_inputs.set_custom_simulation(fuel=False)
+        assert sim_inputs.quic_fire.fuel_flag == 1
+        assert sim_inputs.quic_fire.fuel_density == 0.5
+        assert sim_inputs.quic_fire.fuel_moisture == 0.1
+        assert sim_inputs.quic_fire.fuel_height == 1.0
 
     def test_write_inputs(self):
         sim_inputs = self.get_test_object()
-        sim_inputs.write_inputs("tmp/")
+        sim_inputs.write_inputs(TMP_DIR)
+
+    def test_update_shared_inputs(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.qu_simparams.nx = 100
+        sim_inputs.qu_simparams.ny = 100
+        sim_inputs.quic_fire.nz = 2
+        sim_inputs.quic_fire.time_now = 12345667
+        assert sim_inputs.qu_simparams.nx != sim_inputs.gridlist.n
+        assert sim_inputs.qu_simparams.ny != sim_inputs.gridlist.m
+        assert sim_inputs.quic_fire.nz != sim_inputs.gridlist.l
+        assert sim_inputs.quic_fire.time_now != sim_inputs.sensor1.time_now
+        assert sim_inputs.quic_fire.time_now != sim_inputs.qu_simparams.wind_times[0]
+
+        sim_inputs.write_inputs(TMP_DIR)
+        assert sim_inputs.qu_simparams.nx == sim_inputs.gridlist.n
+        assert sim_inputs.qu_simparams.ny == sim_inputs.gridlist.m
+        assert sim_inputs.quic_fire.nz == sim_inputs.gridlist.l
+        assert sim_inputs.quic_fire.time_now == sim_inputs.sensor1.time_now
+        assert sim_inputs.quic_fire.time_now == sim_inputs.qu_simparams.wind_times[0]
 
     def test_from_directory(self):
-        sim_inputs = SimulationInputs.from_directory("tmp/")
-        assert isinstance(sim_inputs, SimulationInputs)
+        sim_inputs = self.get_test_object()
+        sim_inputs.write_inputs(TMP_DIR)
+        test_object = SimulationInputs.from_directory(TMP_DIR)
+        assert isinstance(test_object, SimulationInputs)
+
+        # Check that the inputs are the same
+        self.compare_simulation_inputs(sim_inputs, test_object)
+
+    def test_to_dict(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.to_dict()
+
+    def test_from_dict(self):
+        sim_inputs = self.get_test_object()
+        sim_dict = sim_inputs.to_dict()
+        test_obj = SimulationInputs.from_dict(sim_dict)
+        assert isinstance(test_obj, SimulationInputs)
+
+        # Check that the inputs are the same
+        self.compare_simulation_inputs(sim_inputs, test_obj)
+
+    def test_to_json(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.to_json(TMP_DIR / "test.json")
+
+    def test_from_json(self):
+        sim_inputs = self.get_test_object()
+        sim_inputs.to_json(TMP_DIR / "test.json")
+        test_obj = SimulationInputs.from_json(TMP_DIR / "test.json")
+        assert isinstance(test_obj, SimulationInputs)
+
+        # Check that the inputs are the same
+        self.compare_simulation_inputs(sim_inputs, test_obj)
diff --git a/tests/test_output_module.py b/tests/test_output_module.py
index d9e4fbb..ca434db 100644
--- a/tests/test_output_module.py
+++ b/tests/test_output_module.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import zarr
 import pytest
 import numpy as np
@@ -10,7 +12,10 @@
 from quicfire_tools import outputs
 
 
-DATA_PATH = Path("data")
+TEST_DIR = Path(__file__).parent
+TMP_DIR = TEST_DIR / "tmp"
+TMP_DIR.mkdir(exist_ok=True)
+DATA_PATH = TEST_DIR / "data"
 OUTPUT_PATH = DATA_PATH / "test-output"
 DRAWFIRE_PATH = OUTPUT_PATH / "drawfire"
 
@@ -41,7 +46,7 @@ def test_zarr_all_outputs(self):
         """
         Run a test to ensure that the zarr output contains all outputs
         """
-        zarr_with_datasets = self.sut.to_zarr("tmp/test.zarr")
+        zarr_with_datasets = self.sut.to_zarr(TMP_DIR / "test.zarr")
         assert isinstance(zarr_with_datasets, zarr.hierarchy.Group)
         for output_name in self.sut.list_available_outputs():
             output = self.sut.get_output(output_name)
@@ -59,7 +64,7 @@ def test_zarr_single_output(self):
         """
         single_output_name = "mburnt_integ"
         zarr_with_datasets = self.sut.to_zarr(
-            "tmp/test.zarr", outputs=single_output_name
+            TMP_DIR / "test.zarr", outputs=single_output_name
         )
         assert isinstance(zarr_with_datasets, zarr.hierarchy.Group)
         for output_name in self.sut.list_available_outputs():
@@ -78,7 +83,7 @@ def test_zarr_multiple_outputs(self):
         """
         multiple_output_names = ["mburnt_integ", "fuels-dens"]
         zarr_with_datasets = self.sut.to_zarr(
-            "tmp/test.zarr", outputs=multiple_output_names
+            TMP_DIR / "test.zarr", outputs=multiple_output_names
         )
         assert isinstance(zarr_with_datasets, zarr.hierarchy.Group)
         for output_name in self.sut.list_available_outputs():
@@ -101,34 +106,34 @@ def test_zarr_xarray_connection(self):
         """
         # Test: All outputs.
         # Produces 4D xarray dataset with all 3D outputs
-        self.sut.to_zarr("tmp/test.zarr")
+        self.sut.to_zarr(TMP_DIR / "test.zarr")
         ds = xr.open_zarr(
-            "tmp/test.zarr", drop_variables=["groundfuelheight", "mburnt_integ"]
+            TMP_DIR / "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)
+        self.sut.to_zarr(TMP_DIR / "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
         ]
-        ds = xr.open_zarr("tmp/test.zarr", drop_variables=drop_variables)
+        ds = xr.open_zarr(TMP_DIR / "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)
+        self.sut.to_zarr(TMP_DIR / "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
         ]
         with pytest.raises(ValueError):
-            xr.open_zarr("tmp/test.zarr", drop_variables=drop_variables)
+            xr.open_zarr(TMP_DIR / "test.zarr", drop_variables=drop_variables)
 
 
 class TestOutputFile: