Repository: PyPSA/PyPSA

Issue:
Make consistency check optionally strict
For some pypsa functions we need input that is already checked in the consistency checks. Therefore it would be good to have the consistency checks optionally in strict mode to raise an error instead of a warning. 

For example, for generalized plotting functions we need colors for all carriers defined. In the context of a soft consistency check this in `n.consistency_check()` would only lead to a warning. But for more complex plotting routines this should be a hard check and would raise an error. The trigger, could be an optional argument `strict`. I then would imagine to run code like  `n.consistency.check_carrier_colors(strict=True)`.


Discussion:

Patch:
diff --git a/doc/references/release-notes.rst b/doc/references/release-notes.rst
index dfb194861..d15465511 100644
--- a/doc/references/release-notes.rst
+++ b/doc/references/release-notes.rst
@@ -40,6 +40,10 @@ Features
     via the `issue tracker <https://github.com/PyPSA/PyPSA/issues/new>`__ on 
     GitHub.
 
+* Consistency checks can now be run with the parameter ``strict``. If set to 
+  ``True``, the consistency check will raise an error if any of the checks fail.  
+
+
 `v0.32.0 <https://github.com/PyPSA/PyPSA/releases/tag/v0.32.0>`__ (5th December 2024)
 =======================================================================================
 
diff --git a/pypsa/consistency.py b/pypsa/consistency.py
index 93c4d028e..1cebc6ce1 100644
--- a/pypsa/consistency.py
+++ b/pypsa/consistency.py
@@ -7,7 +7,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 import numpy as np
 import pandas as pd
@@ -21,12 +21,25 @@
 logger = logging.getLogger(__name__)
 
 
+class ConsistencyError(Exception):
+    pass
+
+
 def _bus_columns(df: pd.DataFrame) -> pd.Index:
     return df.columns[df.columns.str.startswith("bus")]
 
 
+def _log_or_raise(strict: bool, message: str, *args: Any) -> None:
+    if strict:
+        raise ConsistencyError(message, *args)
+    else:
+        logger.warning(message, *args)
+
+
 @deprecated_common_kwargs
-def check_for_unknown_buses(n: Network, component: Components) -> None:
+def check_for_unknown_buses(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check if buses are attached to component but are not defined in the network.
 
@@ -36,6 +49,8 @@ def check_for_unknown_buses(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     for attr in _bus_columns(component.static):
@@ -44,7 +59,8 @@ def check_for_unknown_buses(n: Network, component: Components) -> None:
         if component.name in n.branch_components and int(attr[-1]) > 1:
             missing &= component.static[attr] != ""
         if missing.any():
-            logger.warning(
+            _log_or_raise(
+                strict,
                 "The following %s have buses which are not defined:\n%s",
                 component.list_name,
                 component.static.index[missing],
@@ -52,7 +68,7 @@ def check_for_unknown_buses(n: Network, component: Components) -> None:
 
 
 @deprecated_common_kwargs
-def check_for_disconnected_buses(n: Network) -> None:
+def check_for_disconnected_buses(n: Network, strict: bool = False) -> None:
     """
     Check if network has buses that are not connected to any component.
 
@@ -60,6 +76,8 @@ def check_for_disconnected_buses(n: Network) -> None:
     ----------
     n : pypsa.Network
         The network to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     connected_buses = set()
@@ -69,15 +87,17 @@ def check_for_disconnected_buses(n: Network) -> None:
 
     disconnected_buses = set(n.buses.index) - connected_buses
     if disconnected_buses:
-        logger.warning(
-            "The following buses have no attached components, which can break the "
-            "lopf:\n%s",
+        _log_or_raise(
+            strict,
+            "The following buses have no attached components, which can break the lopf: %s",
             disconnected_buses,
         )
 
 
 @deprecated_common_kwargs
-def check_for_unknown_carriers(n: Network, component: Components) -> None:
+def check_for_unknown_carriers(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check if carriers are attached to component but are not defined in the network.
 
@@ -87,6 +107,8 @@ def check_for_unknown_carriers(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     if "carrier" in component.static.columns:
@@ -96,7 +118,8 @@ def check_for_unknown_carriers(n: Network, component: Components) -> None:
             & (component.static["carrier"] != "")
         )
         if missing.any():
-            logger.warning(
+            _log_or_raise(
+                strict,
                 "The following %s have carriers which are not defined:\n%s",
                 component.list_name,
                 component.static.index[missing],
@@ -104,7 +127,9 @@ def check_for_unknown_carriers(n: Network, component: Components) -> None:
 
 
 @deprecated_common_kwargs
-def check_for_zero_impedances(n: Network, component: Components) -> None:
+def check_for_zero_impedances(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check if component has zero impedances. Only checks passive branch components.
 
@@ -114,15 +139,17 @@ def check_for_zero_impedances(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     if component.name in n.passive_branch_components:
         for attr in ["x", "r"]:
             bad = component.static[attr] == 0
             if bad.any():
-                logger.warning(
-                    "The following %s have zero %s, which "
-                    "could break the linear load flow:\n%s",
+                _log_or_raise(
+                    strict,
+                    "The following %s have zero %s, which could break the linear load flow:\n%s",
                     component.list_name,
                     attr,
                     component.static.index[bad],
@@ -130,7 +157,7 @@ def check_for_zero_impedances(n: Network, component: Components) -> None:
 
 
 @deprecated_common_kwargs
-def check_for_zero_s_nom(component: Components) -> None:
+def check_for_zero_s_nom(component: Components, strict: bool = False) -> None:
     """
     Check if component has zero s_nom. Only checks transformers.
 
@@ -138,22 +165,24 @@ def check_for_zero_s_nom(component: Components) -> None:
     ----------
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     if component.name in {"Transformer"}:
         bad = component.static["s_nom"] == 0
         if bad.any():
-            logger.warning(
-                "The following %s have zero s_nom, which is used "
-                "to define the impedance and will thus break "
-                "the load flow:\n%s",
+            _log_or_raise(
+                strict,
+                "The following %s have zero s_nom, which is used to define the "
+                "impedance and will thus break the load flow:\n%s",
                 component.list_name,
                 component.static.index[bad],
             )
 
 
 @deprecated_common_kwargs
-def check_time_series(n: Network, component: Components) -> None:
+def check_time_series(n: Network, component: Components, strict: bool = False) -> None:
     """
     Check if time series of component are aligned with network snapshots.
 
@@ -163,6 +192,8 @@ def check_time_series(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     for attr in component.attrs.index[component.attrs.varying & component.attrs.static]:
@@ -170,10 +201,10 @@ def check_time_series(n: Network, component: Components) -> None:
 
         diff = attr_df.columns.difference(component.static.index)
         if len(diff):
-            logger.warning(
-                "The following %s have time series defined "
-                "for attribute %s in n.%s_t, but are "
-                "not defined in n.%s:\n%s",
+            _log_or_raise(
+                strict,
+                "The following %s have time series defined for attribute %s in n.%s_t, "
+                "but are not defined in n.%s:\n%s",
                 component.list_name,
                 attr,
                 component.list_name,
@@ -182,16 +213,19 @@ def check_time_series(n: Network, component: Components) -> None:
             )
 
         if not n.snapshots.equals(attr_df.index):
-            logger.warning(
-                "The index of the time-dependent Dataframe for attribute "
-                "%s of n.%s_t is not aligned with network snapshots",
+            _log_or_raise(
+                strict,
+                "The index of the time-dependent Dataframe for attribute %s of n.%s_t "
+                "is not aligned with network snapshots",
                 attr,
                 component.list_name,
             )
 
 
 @deprecated_common_kwargs
-def check_static_power_attributes(n: Network, component: Components) -> None:
+def check_static_power_attributes(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check static attrs p_now, s_nom, e_nom in any component.
 
@@ -201,6 +235,8 @@ def check_static_power_attributes(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     static_attrs = ["p_nom", "s_nom", "e_nom"]
@@ -210,10 +246,10 @@ def check_static_power_attributes(n: Network, component: Components) -> None:
             attr = static_attr[0]
             bad = component.static[attr + "_max"] < component.static[attr + "_min"]
             if bad.any():
-                logger.warning(
-                    "The following %s have smaller maximum than "
-                    "minimum expansion limit which can lead to "
-                    "infeasibilty:\n%s",
+                _log_or_raise(
+                    strict,
+                    "The following %s have smaller maximum than minimum expansion "
+                    "limit which can lead to infeasibility:\n%s",
                     component.list_name,
                     component.static.index[bad],
                 )
@@ -225,7 +261,8 @@ def check_static_power_attributes(n: Network, component: Components) -> None:
                     .isna()
                     .any()
                 ):
-                    logger.warning(
+                    _log_or_raise(
+                        strict,
                         "Encountered nan's in column %s of component '%s'.",
                         col,
                         component.name,
@@ -233,7 +270,9 @@ def check_static_power_attributes(n: Network, component: Components) -> None:
 
 
 @deprecated_common_kwargs
-def check_time_series_power_attributes(n: Network, component: Components) -> None:
+def check_time_series_power_attributes(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check `p_max_pu` and `e_max_pu` nan and infinite values in time series.
 
@@ -243,6 +282,8 @@ def check_time_series_power_attributes(n: Network, component: Components) -> Non
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     varying_attrs = ["p_max_pu", "e_max_pu"]
@@ -259,20 +300,22 @@ def check_time_series_power_attributes(n: Network, component: Components) -> Non
             # check for NaN values:
             if max_pu.isna().to_numpy().any():
                 for col in max_pu.columns[max_pu.isna().any()]:
-                    logger.warning(
-                        "The attribute %s of element %s of %s has "
-                        "NaN values for the following snapshots:\n%s",
-                        attr + "_max_pu",
+                    _log_or_raise(
+                        strict,
+                        "The attribute %s_max_pu of element %s of %s has NaN "
+                        "values for the following snapshots:\n%s",
+                        attr,
                         col,
                         component.list_name,
                         max_pu.index[max_pu[col].isna()],
                     )
             if min_pu.isna().to_numpy().any():
                 for col in min_pu.columns[min_pu.isna().any()]:
-                    logger.warning(
-                        "The attribute %s of element %s of %s has "
-                        "NaN values for the following snapshots:\n%s",
-                        attr + "_min_pu",
+                    _log_or_raise(
+                        strict,
+                        "The attribute %s_min_pu of element %s of %s has NaN "
+                        "values for the following snapshots:\n%s",
+                        attr,
                         col,
                         component.list_name,
                         min_pu.index[min_pu[col].isna()],
@@ -281,20 +324,22 @@ def check_time_series_power_attributes(n: Network, component: Components) -> Non
             # check for infinite values
             if np.isinf(max_pu).to_numpy().any():
                 for col in max_pu.columns[np.isinf(max_pu).any()]:
-                    logger.warning(
-                        "The attribute %s of element %s of %s has "
-                        "infinite values for the following snapshots:\n%s",
-                        attr + "_max_pu",
+                    _log_or_raise(
+                        strict,
+                        "The attribute %s_max_pu of element %s of %s has infinite"
+                        " values for the following snapshots:\n%s",
+                        attr,
                         col,
                         component.list_name,
                         max_pu.index[np.isinf(max_pu[col])],
                     )
             if np.isinf(min_pu).to_numpy().any():
                 for col in min_pu.columns[np.isinf(min_pu).any()]:
-                    logger.warning(
-                        "The attribute %s of element %s of %s has "
-                        "infinite values for the following snapshots:\n%s",
-                        attr + "_min_pu",
+                    _log_or_raise(
+                        strict,
+                        "The attribute %s_min_pu of element %s of %s has infinite"
+                        " values for the following snapshots:\n%s",
+                        attr,
                         col,
                         component.list_name,
                         min_pu.index[np.isinf(min_pu[col])],
@@ -303,10 +348,10 @@ def check_time_series_power_attributes(n: Network, component: Components) -> Non
             diff = max_pu - min_pu
             diff = diff[diff < 0].dropna(axis=1, how="all")
             for col in diff.columns:
-                logger.warning(
-                    "The element %s of %s has a smaller maximum "
-                    "than minimum operational limit which can "
-                    "lead to infeasibility for the following snapshots:\n%s",
+                _log_or_raise(
+                    strict,
+                    "The element %s of %s has a smaller maximum than minimum operational"
+                    " limit which can lead to infeasibility for the following snapshots:\n%s",
                     col,
                     component.list_name,
                     diff[col].dropna().index,
@@ -314,7 +359,7 @@ def check_time_series_power_attributes(n: Network, component: Components) -> Non
 
 
 @deprecated_common_kwargs
-def check_assets(n: Network, component: Components) -> None:
+def check_assets(n: Network, component: Components, strict: bool = False) -> None:
     """
     Check if assets are only committable or extendable, but not both.
 
@@ -324,6 +369,8 @@ def check_assets(n: Network, component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     if component.name in {"Generator", "Link"}:
@@ -331,15 +378,17 @@ def check_assets(n: Network, component: Components) -> None:
         extendables = n.get_extendable_i(component.name)
         intersection = committables.intersection(extendables)
         if not intersection.empty:
-            logger.warning(
-                "Assets can only be committable or extendable. Found "
-                f"assets in component {component.name} which are both:"
-                f"\n\n\t{', '.join(intersection)}"
+            _log_or_raise(
+                strict,
+                "Assets can only be committable or extendable."
+                " Found assets in component %s which are both:\n\n\t%s",
+                component.name,
+                ", ".join(intersection),
             )
 
 
 @deprecated_common_kwargs
-def check_generators(component: Components) -> None:
+def check_generators(component: Components, strict: bool = False) -> None:
     """
     Check the consistency of generator attributes before the simulation.
 
@@ -351,6 +400,8 @@ def check_generators(component: Components) -> None:
     ----------
     component : Component
         The generator component to be checked.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     Returns
     -------
@@ -364,25 +415,27 @@ def check_generators(component: Components) -> None:
             & (component.static.down_time_before > 0)
         ]
         if not bad_uc_gens.empty:
-            logger.warning(
-                "The following committable generators were both up and down"
-                f" before the simulation: {bad_uc_gens}."
-                " This could cause an infeasibility."
+            _log_or_raise(
+                strict,
+                "The following committable generators were both"
+                " up and down before the simulation: %s. This could cause an infeasibility.",
+                bad_uc_gens,
             )
 
         bad_e_sum_gens = component.static.index[
             component.static.e_sum_min > component.static.e_sum_max
         ]
         if not bad_e_sum_gens.empty:
-            logger.warning(
-                "The following generators have e_sum_min > e_sum_max, "
-                "which can lead to infeasibility:\n"
-                f"{bad_e_sum_gens}.",
+            _log_or_raise(
+                strict,
+                "The following generators have e_sum_min > e_sum_max,"
+                " which can lead to infeasibility:\n%s.",
+                bad_e_sum_gens,
             )
 
 
 @deprecated_common_kwargs
-def check_dtypes_(component: Components) -> None:
+def check_dtypes_(component: Components, strict: bool = False) -> None:
     """
     Check if the dtypes of the attributes in the component are as expected.
 
@@ -392,16 +445,18 @@ def check_dtypes_(component: Components) -> None:
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     dtypes_soll = component.attrs.loc[component.attrs["static"], "dtype"].drop("name")
     unmatched = component.static.dtypes[dtypes_soll.index] != dtypes_soll
 
     if unmatched.any():
-        logger.warning(
-            "The following attributes of the dataframe %s "
-            "have the wrong dtype:\n%s\n"
-            "They are:\n%s\nbut should be:\n%s",
+        _log_or_raise(
+            strict,
+            "The following attributes of the dataframe %s have"
+            " the wrong dtype:\n%s\nThey are:\n%s\nbut should be:\n%s",
             component.list_name,
             unmatched.index[unmatched],
             component.static.dtypes[dtypes_soll.index[unmatched]],
@@ -419,10 +474,10 @@ def check_dtypes_(component: Components) -> None:
         unmatched = component.dynamic[attr].dtypes != dtype
 
         if unmatched.any():
-            logger.warning(
-                "The following columns of time-varying attribute "
-                "%s in %s_t have the wrong dtype:\n%s\n"
-                "They are:\n%s\nbut should be:\n%s",
+            _log_or_raise(
+                strict,
+                "The following columns of time-varying attribute %s in %s_t"
+                " have the wrong dtype:\n%s\nThey are:\n%s\nbut should be:\n%s",
                 attr,
                 component.list_name,
                 unmatched.index[unmatched],
@@ -432,7 +487,7 @@ def check_dtypes_(component: Components) -> None:
 
 
 @deprecated_common_kwargs
-def check_investment_periods(n: Network) -> None:
+def check_investment_periods(n: Network, strict: bool = False) -> None:
     """
     Check if investment periods are aligned with snapshots.
 
@@ -440,6 +495,8 @@ def check_investment_periods(n: Network) -> None:
     ----------
     n : pypsa.Network
         The network to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     constraint_periods = set(n.global_constraints.investment_period.dropna().unique())
@@ -449,18 +506,24 @@ def check_investment_periods(n: Network) -> None:
                 "The global constraints contain investment periods which "
                 "are not in the set of optimized snapshots."
             )
-            raise ValueError(msg)
+            if strict:
+                raise ValueError(msg)
+            else:
+                _log_or_raise(strict, msg)
     else:
         if constraint_periods:
             msg = (
                 "The global constraints contain investment periods but "
                 "snapshots are not multi-indexed."
             )
-            raise ValueError(msg)
+            if strict:
+                raise ValueError(msg)
+            else:
+                _log_or_raise(strict, msg)
 
 
 @deprecated_common_kwargs
-def check_shapes(n: Network) -> None:
+def check_shapes(n: Network, strict: bool = False) -> None:
     """
     Check if shapes are aligned with related components.
 
@@ -468,8 +531,8 @@ def check_shapes(n: Network) -> None:
     ----------
     n : pypsa.Network
         The network to check.
-    component : pypsa.Component
-        The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     shape_components = n.shapes.component.unique()
@@ -478,15 +541,19 @@ def check_shapes(n: Network) -> None:
         not_included = geos.index[~geos.idx.isin(n.static(c).index)]
 
         if not not_included.empty:
-            logger.warning(
-                f"The following shapes are related to component {c} and have"
-                f" idx values that are not included in the component's index:\n"
-                f"{not_included}"
+            _log_or_raise(
+                strict,
+                "The following shapes are related to component %s and"
+                " have idx values that are not included in the component's index:\n%s",
+                c,
+                not_included,
             )
 
 
 @deprecated_common_kwargs
-def check_nans_for_component_default_attrs(n: Network, component: Components) -> None:
+def check_nans_for_component_default_attrs(
+    n: Network, component: Components, strict: bool = False
+) -> None:
     """
     Check for missing values in component attributes.
 
@@ -499,6 +566,8 @@ def check_nans_for_component_default_attrs(n: Network, component: Components) ->
         The network to check.
     component : pypsa.Component
         The component to check.
+    strict : bool, optional
+        If True, raise an error instead of logging a warning.
 
     """
     # Get non-NA and not-empty default attributes for the current component
@@ -515,7 +584,8 @@ def check_nans_for_component_default_attrs(n: Network, component: Components) ->
     # Run the check for nan values on relevant static data
     if (isna := relevant_static_df.isna().any()).any():
         nan_cols = relevant_static_df.columns[isna]
-        logger.warning(
+        _log_or_raise(
+            strict,
             "Encountered nan's in static data for columns %s of component '%s'.",
             nan_cols.to_list(),
             component.name,
@@ -533,7 +603,8 @@ def check_nans_for_component_default_attrs(n: Network, component: Components) ->
     for key, values_df in relevant_series_dfs.items():
         if (isna := values_df.isna().any()).any():
             nan_cols = values_df.columns[isna]
-            logger.warning(
+            _log_or_raise(
+                strict,
                 "Encountered nan's in varying data '%s' for columns %s of component '%s'.",
                 key,
                 nan_cols.to_list(),
diff --git a/pypsa/networks.py b/pypsa/networks.py
index d2ac4f3ea..2e5fc5c63 100644
--- a/pypsa/networks.py
+++ b/pypsa/networks.py
@@ -1519,7 +1519,9 @@ def iterate_components(
             if not (skip_empty and self.static(c_name).empty)
         )
 
-    def consistency_check(self, check_dtypes: bool = False) -> None:
+    def consistency_check(
+        self, check_dtypes: bool = False, strict: bool = False
+    ) -> None:
         """
         Checks the network for consistency; e.g. that all components are
         connected to existing buses and that no impedances are singular.
@@ -1539,28 +1541,28 @@ def consistency_check(self, check_dtypes: bool = False) -> None:
         # Per component checks
         for c in self.iterate_components():
             # Checks all components
-            check_for_unknown_buses(self, c)
-            check_for_unknown_carriers(self, c)
-            check_time_series(self, c)
-            check_static_power_attributes(self, c)
-            check_time_series_power_attributes(self, c)
-            check_nans_for_component_default_attrs(self, c)
+            check_for_unknown_buses(self, c, strict)
+            check_for_unknown_carriers(self, c, strict)
+            check_time_series(self, c, strict)
+            check_static_power_attributes(self, c, strict)
+            check_time_series_power_attributes(self, c, strict)
+            check_nans_for_component_default_attrs(self, c, strict)
             # Checks passive_branch_components
-            check_for_zero_impedances(self, c)
+            check_for_zero_impedances(self, c, strict)
             # Checks transformers
-            check_for_zero_s_nom(c)
+            check_for_zero_s_nom(c, strict)
             # Checks generators and links
-            check_assets(self, c)
+            check_assets(self, c, strict)
             # Checks generators
-            check_generators(c)
+            check_generators(c, strict)
 
             if check_dtypes:
-                check_dtypes_(c)
+                check_dtypes_(c, strict)
 
         # Combined checks
-        check_for_disconnected_buses(self)
-        check_investment_periods(self)
-        check_shapes(self)
+        check_for_disconnected_buses(self, strict)
+        check_investment_periods(self, strict)
+        check_shapes(self, strict)
 
 
 class SubNetwork:


Test patch:
diff --git a/test/test_consistency_check.py b/test/test_consistency_check.py
index 8561d0ef6..5f53b6152 100644
--- a/test/test_consistency_check.py
+++ b/test/test_consistency_check.py
@@ -11,6 +11,16 @@
 import pytest
 
 import pypsa
+import pypsa.consistency
+
+
+def assert_log_or_error_in_consistency(n, caplog, strict=False):
+    if strict:
+        with pytest.raises(pypsa.consistency.ConsistencyError):
+            n.consistency_check(strict=strict)
+    else:
+        n.consistency_check(strict=strict)
+        assert caplog.records[-1].levelname == "WARNING"
 
 
 @pytest.fixture
@@ -24,35 +34,40 @@ def consistent_n():
     return n
 
 
+@pytest.mark.parametrize("strict", [True, False])
 @pytest.mark.skipif(os.name == "nt", reason="dtype confusing on Windows")
-def test_consistency(consistent_n, caplog):
-    consistent_n.consistency_check()
-    assert not caplog.records
+def test_consistency(consistent_n, caplog, strict):
+    if strict:
+        consistent_n.consistency_check(strict=strict)
+    else:
+        consistent_n.consistency_check()
+        assert not caplog.records
 
 
-def test_missing_bus(consistent_n, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_missing_bus(consistent_n, caplog, strict):
     consistent_n.add("Bus", "three")
-    consistent_n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
+    assert_log_or_error_in_consistency(consistent_n, caplog, strict=strict)
 
 
-def test_infeasible_capacity_limits(consistent_n, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_infeasible_capacity_limits(consistent_n, caplog, strict):
     consistent_n.generators.loc["gen_one", ["p_nom_extendable", "committable"]] = (
         True,
         True,
     )
-    consistent_n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
+    assert_log_or_error_in_consistency(consistent_n, caplog, strict=strict)
 
 
-def test_nans_in_capacity_limits(consistent_n, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_nans_in_capacity_limits(consistent_n, caplog, strict):
     consistent_n.generators.loc["gen_one", "p_nom_extendable"] = True
     consistent_n.generators.loc["gen_one", "p_nom_max"] = np.nan
-    consistent_n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
+    assert_log_or_error_in_consistency(consistent_n, caplog, strict=strict)
 
 
-def test_shapes_with_missing_idx(ac_dc_network_shapes, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_shapes_with_missing_idx(ac_dc_network_shapes, caplog, strict):
     n = ac_dc_network_shapes
     n.add(
         "Shape",
@@ -61,23 +76,21 @@ def test_shapes_with_missing_idx(ac_dc_network_shapes, caplog):
         component="Bus",
         idx="missing_idx",
     )
-    n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
-    assert caplog.records[-1].message.startswith("The following shapes")
+    assert_log_or_error_in_consistency(ac_dc_network_shapes, caplog, strict=strict)
 
 
-def test_unknown_carriers(consistent_n, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_unknown_carriers(consistent_n, caplog, strict):
     consistent_n.add("Generator", "wind", bus="hub", carrier="wind")
-    consistent_n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
+    assert_log_or_error_in_consistency(consistent_n, caplog, strict=strict)
 
 
-def test_inconsistent_e_sum_values(consistent_n, caplog):
+@pytest.mark.parametrize("strict", [True, False])
+def test_inconsistent_e_sum_values(consistent_n, caplog, strict):
     """
     Test that the consistency check raises a warning if the e_sum_min is greater than e_sum_max.
     """
     consistent_n.add(
         "Generator", "gen_two", bus="one", p_nom_max=10, e_sum_min=10, e_sum_max=5
     )
-    consistent_n.consistency_check()
-    assert caplog.records[-1].levelname == "WARNING"
+    assert_log_or_error_in_consistency(consistent_n, caplog, strict=strict)