diff --git a/HRM_Processor.py b/HRM_Processor.py
index 20de108..c7487c2 100644
--- a/HRM_Processor.py
+++ b/HRM_Processor.py
@@ -16,6 +16,10 @@ class HRM_Processor:
     output_dict: dict
                 A dictionary containing the heart rate metrics that will be
                 written to the JSON file by the DataWriter.
+    is_realistic:   boolean
+                    True if the number of beats is realistic for the ECG
+                    strip duration. Used by the DataWriter to determine if
+                    the data should be written to the JSON file.
 
     """
     def __init__(self, DataReader):
@@ -32,7 +36,14 @@ class HRM_Processor:
         self.input_data = DataReader.output_dict
         self.output_dict = {}
         self.write_outputs_to_dict()
-        self.validate_outputs(self.output_dict)
+        try:
+            self.validate_outputs(self.output_dict)
+        except ValueError:
+            print("The heart rate monitor has calculated a heart rate that "
+                  "is not physiologically realistic. This is likely due to "
+                  "a high level of signal noise. These misleading / "
+                  "inaccurate outputs will not be written to a JSON file.")
+            self.isValid = False
 
     def write_outputs_to_dict(self):
         """Writes all of the HRM_Processor's outputs to it's output
@@ -81,6 +92,12 @@ class HRM_Processor:
 
         """
         self.check_voltage_extremes(output_dict["voltage_extremes"])
+        is_realistic = self.is_unrealistic_num_beats(output_dict["num_beats"],
+                                                     output_dict["duration"])
+
+        if not is_realistic:
+            raise ValueError
+        self.isValid = True
 
     def determine_voltage_extremes(self, voltage):
         """Determines the min and max values of the voltage data
@@ -326,6 +343,37 @@ class HRM_Processor:
         num_beats = np.size(beat_start_times)
         return num_beats
 
+    def is_unrealistic_num_beats(self, num_beats, duration):
+        """Checks that the calculated number of beats is physiologically
+        realistic for the duration of the ECG strip.
+
+        Physiologically realistic for the sake of this function is defined
+        as a heart rate that is greater than 150 BPM or one that is less tha 40
+        BPM. While it may be possible in some extreme cases for heart rates
+        to exceed these extremes, it is considered more likely that the beat
+        detection algorithm is falsely detecting beats or missing beats.
+
+        Parameters
+        ----------
+        num_beats:  int
+                    The number of beats in the ECG strip
+        duration:   float
+                    The duration of the ECG strip
+
+        Returns
+        -------
+        realistic_num_beats:    boolean
+                                Specifies whether the number of beats is
+                                physiologically realistic.
+        """
+        realistic_max_beats = 2.5*duration  # corresponds to 150 BPM
+        realistic_min_beats = 0.6*duration  # corresponds to 36 BPM
+
+        if num_beats > realistic_max_beats or num_beats < realistic_min_beats:
+            return False
+        else:
+            return True
+
     def determine_bpm(self, beat_start_times, duration):
         """Determines the mean beats per minute (BPM) within a specified
         duration of time.

The HRM_Processor should fail gracefully when it calculates unrealistic numbers of beats.
In situations where the beat detection algorithm calculates heart rate values that are unrealistic, it is more likely that the algorithm failed due to noisy data. In these cases, the program should not output a JSON file, and instead tell the user that it failed. 
diff --git a/test_HRM_Processor.py b/test_HRM_Processor.py
index b381ca3..8f67a65 100644
--- a/test_HRM_Processor.py
+++ b/test_HRM_Processor.py
@@ -336,3 +336,59 @@ def test_check_voltage_extremes_no_warnings(hrm, capsys, voltage_extremes):
     captured_output, err = capsys.readouterr()
 
     assert captured_output == ''
+
+
+@pytest.mark.parametrize("num_beats, duration, is_realistic", [
+    (50, 60.0, True),
+    (60, 60.0, True),
+    (151, 60.0, False),
+    (35, 60.0, False),
+])
+def test_is_realistic_num_beats(hrm, num_beats, duration, is_realistic):
+    """Tests that the is_realistic_num_beats correctly determines is the
+    number of beats is physiologically realistic for a given strip duration.
+
+    THe duration of the strip is measured in seconds. An unrealistic number
+    of beats is defined as less than 36 BPM or greater than 150 BPM.
+
+    Parameters
+    ----------
+    hrm:        HRM_Processor
+                A generic HRM_Processor made from a DataReader with
+                test_file.csv
+    num_beats:  int
+                The number of beats in a ECG sequence
+    duration:   float
+                The duration of the ECG sequence in seconds
+    is_realistic:   boolean
+                    True if the number of beats is physiologically realistic.
+
+    Returns
+    -------
+    None
+    """
+    measured_is_realistic = hrm.is_unrealistic_num_beats(num_beats, duration)
+
+    assert measured_is_realistic == is_realistic
+
+
+def test_validate_outputs_invalid_case(hrm):
+    """Checks that the validate_outputs function raises a ValueError when
+    the number of beats is not physiologically realistic for a ECG strip
+    duration.
+
+    Parameters
+    ----------
+    hrm:    HRM_Processor
+            A generic HRM_Processor object made from a DataReader with
+            test_file.csv
+
+    Returns
+    -------
+    None
+    """
+    output_dict = {"num_beats": 10000, "duration": 60, "voltage_extremes": (
+        0, 200)}
+
+    with pytest.raises(ValueError):
+        hrm.validate_outputs(output_dict)

pip install -r requirements.txt
parse_log_pytest
requirements.txt
pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
exceptiongroup==1.2.2
execnet==2.1.1
iniconfig==2.1.0
numpy==2.0.2
packaging==24.2
pep8==1.7.1
pluggy==1.5.0
pytest==8.3.5
pytest-cache==1.0
pytest-cov==6.0.0
pytest-pep8==1.0.6
scipy==1.13.1
tomli==2.2.1

name: bme590hrm
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - iniconfig==2.1.0
      - numpy==2.0.2
      - packaging==24.2
      - pep8==1.7.1
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-cache==1.0
      - pytest-cov==6.0.0
      - pytest-pep8==1.0.6
      - scipy==1.13.1
      - tomli==2.2.1
prefix: /opt/conda/envs/bme590hrm

test_HRM_Processor.py::test_is_realistic_num_beats[50-60.0-True] test_HRM_Processor.py::test_is_realistic_num_beats[60-60.0-True] test_HRM_Processor.py::test_is_realistic_num_beats[151-60.0-False] test_HRM_Processor.py::test_is_realistic_num_beats[35-60.0-False] test_HRM_Processor.py::test_validate_outputs_invalid_case
test_HRM_Processor.py::test_find_qrs_peak_indices
test_HRM_Processor.py::test_HRM_Processor_init test_HRM_Processor.py::test_voltage_extremes test_HRM_Processor.py::test_write_outputs_to_dict_voltage_extremes test_HRM_Processor.py::test_determine_ecg_strip_duration test_HRM_Processor.py::test_write_strip_duration test_HRM_Processor.py::test_determine_threshold test_HRM_Processor.py::test_find_indices_above_threshold test_HRM_Processor.py::test_find_beat_separation_points test_HRM_Processor.py::test_index_beat_start_times test_HRM_Processor.py::test_determine_num_beats test_HRM_Processor.py::test_determine_bpm test_HRM_Processor.py::test_determine_bpm2 test_HRM_Processor.py::test_check_voltage_extremes_warnings[voltage_extremes0] test_HRM_Processor.py::test_check_voltage_extremes_warnings[voltage_extremes1] test_HRM_Processor.py::test_check_voltage_extremes_warnings[voltage_extremes2] test_HRM_Processor.py::test_check_voltage_extremes_warnings[voltage_extremes3] test_HRM_Processor.py::test_check_voltage_extremes_no_warnings[voltage_extremes0] test_HRM_Processor.py::test_check_voltage_extremes_no_warnings[voltage_extremes1] test_HRM_Processor.py::test_check_voltage_extremes_no_warnings[voltage_extremes2] test_HRM_Processor.py::test_check_voltage_extremes_no_warnings[voltage_extremes3]
HRM_Processor.py
HRM_Processor.py