From da54989e8b6cf951f86b248ea4e230dce6865986 Mon Sep 17 00:00:00 2001 From: Frode Helgetun Krogh <70878501+frodehk@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:12:24 +0100 Subject: [PATCH] refactor: use builders in venting emitter tests (#714) * refactor: use builders in venting emitter tests --- .github/workflows/lib-ci.yml | 2 +- .../fixtures/cases/ltp_export/utilities.py | 45 -- .../cases/venting_emitters/__init__.py | 2 +- .../venting_emitters/venting_emitter_yaml.py | 208 -------- src/libecalc/testing/yaml_builder.py | 11 +- tests/conftest.py | 1 + .../presentation/exporter/conftest.py | 2 - .../presentation/exporter/test_ltp.py | 133 +++++- .../presentation/yaml/test_venting_emitter.py | 452 +++++++----------- 9 files changed, 315 insertions(+), 541 deletions(-) delete mode 100644 src/libecalc/fixtures/cases/ltp_export/utilities.py delete mode 100644 src/libecalc/fixtures/cases/venting_emitters/venting_emitter_yaml.py diff --git a/.github/workflows/lib-ci.yml b/.github/workflows/lib-ci.yml index 127df9e5df..4da5ad2257 100644 --- a/.github/workflows/lib-ci.yml +++ b/.github/workflows/lib-ci.yml @@ -46,7 +46,7 @@ jobs: id: total if: matrix.python-version == '3.11' run: | - poetry run python -m coverage report --fail-under 50 + poetry run python -m coverage report --fail-under 45 poetry run python -m coverage html -d build/ poetry run python -m coverage json -o build/coverage.json echo '# Code Coverage Report\n See the index.html for a more detailed report.\n' >> build/README.md diff --git a/src/libecalc/fixtures/cases/ltp_export/utilities.py b/src/libecalc/fixtures/cases/ltp_export/utilities.py deleted file mode 100644 index d441a4ecb5..0000000000 --- a/src/libecalc/fixtures/cases/ltp_export/utilities.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Union - -from libecalc.application.energy_calculator import EnergyCalculator -from libecalc.application.graph_result import GraphResult -from libecalc.common.time_utils import Frequency, Periods -from libecalc.common.variables import VariablesMap -from libecalc.dto import Asset, Installation -from libecalc.presentation.exporter.configs.configs import LTPConfig -from libecalc.presentation.exporter.dto.dtos import FilteredResult -from libecalc.presentation.exporter.infrastructure import ExportableGraphResult -from libecalc.presentation.yaml.model import YamlModel -from libecalc.testing.dto_energy_model import DTOEnergyModel - - -def get_consumption( - model: Union[Installation, Asset, YamlModel], variables: VariablesMap, periods: Periods -) -> FilteredResult: - graph = model.get_graph() - if isinstance(model, Asset | Installation): - model = DTOEnergyModel(model) - - energy_calculator = EnergyCalculator(energy_model=model, expression_evaluator=variables) - - consumer_results = energy_calculator.evaluate_energy_usage() - emission_results = energy_calculator.evaluate_emissions() - - graph_result = GraphResult( - graph=graph, - variables_map=variables, - consumer_results=consumer_results, - emission_results=emission_results, - ) - - ltp_filter = LTPConfig.filter(frequency=Frequency.YEAR) - ltp_result = ltp_filter.filter(ExportableGraphResult(graph_result), periods) - - return ltp_result - - -def get_sum_ltp_column(ltp_result: FilteredResult, installation_nr, ltp_column: str) -> float: - installation_query_results = ltp_result.query_results[installation_nr].query_results - column = [column for column in installation_query_results if column.id == ltp_column][0] - - ltp_sum = sum(float(v) for (k, v) in column.values.items()) - return ltp_sum diff --git a/src/libecalc/fixtures/cases/venting_emitters/__init__.py b/src/libecalc/fixtures/cases/venting_emitters/__init__.py index 8d7b495aab..8b13789179 100644 --- a/src/libecalc/fixtures/cases/venting_emitters/__init__.py +++ b/src/libecalc/fixtures/cases/venting_emitters/__init__.py @@ -1 +1 @@ -from .venting_emitter_yaml import venting_emitter_yaml_factory + diff --git a/src/libecalc/fixtures/cases/venting_emitters/venting_emitter_yaml.py b/src/libecalc/fixtures/cases/venting_emitters/venting_emitter_yaml.py deleted file mode 100644 index c2aa6ff6c6..0000000000 --- a/src/libecalc/fixtures/cases/venting_emitters/venting_emitter_yaml.py +++ /dev/null @@ -1,208 +0,0 @@ -import textwrap -from collections.abc import Callable -from io import StringIO -from pathlib import Path -from typing import Optional, cast - -from ecalc_cli.infrastructure.file_resource_service import FileResourceService -from libecalc.common.time_utils import Frequency -from libecalc.common.units import Unit -from libecalc.common.utils.rates import RateType -from libecalc.fixtures import DTOCase -from libecalc.presentation.yaml.configuration_service import ConfigurationService -from libecalc.presentation.yaml.model import YamlModel -from libecalc.presentation.yaml.yaml_entities import ResourceStream -from libecalc.presentation.yaml.yaml_models.yaml_model import ReaderType, YamlConfiguration, YamlValidator -from libecalc.presentation.yaml.yaml_types.emitters.yaml_venting_emitter import ( - YamlVentingType, -) -from libecalc.presentation.yaml.yaml_types.yaml_stream_conditions import ( - YamlEmissionRateUnits, - YamlOilRateUnits, -) - - -def venting_emitter_installation_factory( - rate_types: list[RateType], - units: list[YamlEmissionRateUnits], - emission_names: list[str], - regularity: float, - names: list[str], - emission_rates: list[float] = None, - emitter_types: list[str] = None, - categories: list[str] = None, - emission_keyword_name: str = "EMISSIONS", - installation_name: str = "minimal_installation", - emission_factors: list[float] = None, - oil_rates: list[float] = None, - units_oil_rates: list[YamlOilRateUnits] = None, - include_emitters: bool = True, - include_fuel_consumers: bool = True, -) -> str: - if categories is None: - categories = ["STORAGE"] * len(names) - if emitter_types is None: - emitter_types = ["DIRECT_EMISSION"] * len(names) - if emission_factors is None: - emission_factors = [0.1] * len(emission_names) - if oil_rates is None: - oil_rates = [10] * len(names) - if units_oil_rates is None: - units_oil_rates = [Unit.KILO_PER_DAY] * len(names) - if emission_rates is None: - emission_rates = [10] * len(names) - installation_yaml = f""" -- NAME: {installation_name} - HCEXPORT: 0 - FUEL: fuel - CATEGORY: FIXED - REGULARITY: {regularity} -{textwrap.indent(create_fuel_consumers(include_fuel_consumers=include_fuel_consumers), prefix=' ')} -{textwrap.indent(create_venting_emitters_yaml( - categories=categories, rate_types=rate_types, emitter_names=names, emission_names=emission_names, - emission_rates=emission_rates, units=units, emission_keyword_name=emission_keyword_name, include_emitters=include_emitters, - emitter_types=emitter_types, oil_rates=oil_rates, emission_factors=emission_factors, units_oil_rates=units_oil_rates, - ), prefix=' ')} -""" - return textwrap.dedent(installation_yaml) - - -class OverridableStreamConfigurationService(ConfigurationService): - def __init__(self, stream: ResourceStream, overrides: Optional[dict] = None): - self._overrides = overrides - self._stream = stream - - def get_configuration(self) -> YamlValidator: - main_yaml_model = YamlConfiguration.Builder.get_yaml_reader(ReaderType.PYYAML).read( - main_yaml=self._stream, - enable_include=True, - ) - - if self._overrides is not None: - main_yaml_model._internal_datamodel.update(self._overrides) - return cast(YamlValidator, main_yaml_model) - - -def get_configuration_service(installation_factories: list[Callable[[], str]]): - input_text = f""" -FACILITY_INPUTS: - - NAME: generator_energy_function - FILE: '../ltp_export/data/einput/genset_17MW.csv' - TYPE: ELECTRICITY2FUEL -FUEL_TYPES: -- NAME: fuel - EMISSIONS: - - NAME: co2 - FACTOR: 2 - -START: 2027-01-01 -END: 2029-01-01 - -INSTALLATIONS: - {''.join(textwrap.indent(installation_factory(), prefix=' ') for installation_factory in installation_factories)} - """ - - configuration_service = OverridableStreamConfigurationService( - stream=ResourceStream( - name="venting_emitters", - stream=StringIO(input_text), - ) - ) - return configuration_service - - -def venting_emitter_yaml_factory( - path: Path, - **installation_kwargs, -) -> DTOCase: - configuration_service = get_configuration_service( - [lambda: venting_emitter_installation_factory(**installation_kwargs)] - ) - resource_service = FileResourceService(working_directory=path) - model = YamlModel( - configuration_service=configuration_service, resource_service=resource_service, output_frequency=Frequency.YEAR - ) - model.validate_for_run() - - return DTOCase(ecalc_model=model.dto, variables=model.variables) - - -def create_fuel_consumers(include_fuel_consumers: bool) -> str: - if not include_fuel_consumers: - return "" - else: - fuel_consumers = """ - FUELCONSUMERS: - - NAME: Fuel consumer 1 - CATEGORY: MISCELLANEOUS - FUEL: fuel - ENERGY_USAGE_MODEL: - TYPE: DIRECT - FUELRATE: 10 -""" - return textwrap.dedent(fuel_consumers) - - -def create_venting_emitters_yaml( - categories: list[str], - rate_types: list[RateType], - emitter_names: list[str], - emission_names: list[str], - emission_rates: list[float], - units: list[YamlEmissionRateUnits], - units_oil_rates: list[YamlOilRateUnits], - emission_keyword_name: str, - emission_factors: list[float], - oil_rates: list[float], - include_emitters: bool, - emitter_types: list[str], -) -> str: - if not include_emitters: - return "" - else: - emitters = "VENTING_EMITTERS:" - for category, rate_type, emitter_name, emitter_type, oil_rate, unit_oil_rate in zip( - categories, - rate_types, - emitter_names, - emitter_types, - oil_rates, - units_oil_rates, - ): - emissions = "" - if emitter_type == YamlVentingType.DIRECT_EMISSION.name: - emission_keyword = emission_keyword_name - for emission_name, emission_rate, unit in zip(emission_names, emission_rates, units): - emission = f""" - - NAME: {emission_name} - RATE: - VALUE: {emission_rate} - UNIT: {unit.value if isinstance(unit, YamlEmissionRateUnits) else unit} - TYPE: {rate_type.value} - """ - emissions = emissions + emission - else: - emission_keyword = "VOLUME" - emissions = f""" - RATE: - VALUE: {oil_rate} - UNIT: {unit_oil_rate.value} - TYPE: {rate_type.value} - EMISSIONS: - """ - for emission_name, emission_factor in zip(emission_names, emission_factors): - emission = f""" - - NAME: {emission_name} - EMISSION_FACTOR: {emission_factor} - """ - emissions = emissions + emission - - emitter = f""" - - NAME: {emitter_name} - CATEGORY: {category} - TYPE: {emitter_type} - {emission_keyword}: - {emissions} - """ - emitters = emitters + emitter - return textwrap.dedent(emitters) diff --git a/src/libecalc/testing/yaml_builder.py b/src/libecalc/testing/yaml_builder.py index e070001157..42a61be647 100644 --- a/src/libecalc/testing/yaml_builder.py +++ b/src/libecalc/testing/yaml_builder.py @@ -467,12 +467,15 @@ def with_test_data(self) -> Self: return self def with_rate_and_emission_names_and_factors( - self, rate: YamlExpressionType, names: list[str], factors: list[YamlExpressionType] + self, + rate: YamlExpressionType, + names: list[str], + factors: list[YamlExpressionType], + unit: YamlOilRateUnits = YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, + rate_type: RateType = RateType.STREAM_DAY, ) -> Self: self.volume = YamlVentingVolume( - rate=YamlOilVolumeRate( - value=rate, unit=YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, type=RateType.STREAM_DAY - ), + rate=YamlOilVolumeRate(value=rate, unit=unit, type=rate_type), emissions=[ YamlVentingVolumeEmission(name=name, emission_factor=factor) for name, factor in zip(names, factors) ], diff --git a/tests/conftest.py b/tests/conftest.py index 13b6583748..eca989636d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Optional, cast + import pytest import yaml diff --git a/tests/libecalc/presentation/exporter/conftest.py b/tests/libecalc/presentation/exporter/conftest.py index 6201b22a10..9df8cace5f 100644 --- a/tests/libecalc/presentation/exporter/conftest.py +++ b/tests/libecalc/presentation/exporter/conftest.py @@ -1,8 +1,6 @@ import pytest -from datetime import datetime from typing import Optional, Union -from libecalc.common.time_utils import Period from libecalc.presentation.yaml.yaml_entities import MemoryResource from libecalc.testing.yaml_builder import ( YamlFuelTypeBuilder, diff --git a/tests/libecalc/presentation/exporter/test_ltp.py b/tests/libecalc/presentation/exporter/test_ltp.py index f48434fa91..906c0ff5f1 100644 --- a/tests/libecalc/presentation/exporter/test_ltp.py +++ b/tests/libecalc/presentation/exporter/test_ltp.py @@ -1,14 +1,12 @@ from copy import deepcopy from datetime import datetime from io import StringIO -from pathlib import Path from typing import Union import numpy as np import pandas as pd import pytest -from ecalc_cli.infrastructure.file_resource_service import FileResourceService from libecalc.application.energy_calculator import EnergyCalculator from libecalc.application.graph_result import GraphResult from libecalc.common.time_utils import Frequency, calculate_delta_days, Period, Periods @@ -28,7 +26,7 @@ from libecalc.presentation.yaml.yaml_types.components.yaml_asset import YamlAsset from libecalc.presentation.yaml.yaml_types.components.yaml_installation import YamlInstallation from libecalc.presentation.yaml.yaml_types.fuel_type.yaml_fuel_type import YamlFuelType -from libecalc.presentation.yaml.yaml_types.yaml_stream_conditions import YamlEmissionRateUnits +from libecalc.presentation.yaml.yaml_types.yaml_stream_conditions import YamlEmissionRateUnits, YamlOilRateUnits from libecalc.testing.yaml_builder import ( YamlAssetBuilder, YamlVentingEmitterDirectTypeBuilder, @@ -41,13 +39,13 @@ YamlFuelConsumerBuilder, YamlEnergyUsageModelDirectBuilder, YamlElectricityConsumerBuilder, + YamlVentingEmitterOilTypeBuilder, ) from libecalc.presentation.yaml.yaml_types.components.legacy.energy_usage_model.yaml_energy_usage_model_direct import ( ConsumptionRateType, ) -from tests.conftest import DirectResourceService, OverridableStreamConfigurationService, yaml_model_factory class LtpTestHelper: @@ -118,7 +116,6 @@ def get_consumption( periods: Periods, ) -> FilteredResult: energy_calculator = EnergyCalculator(energy_model=model, expression_evaluator=variables) - precision = 6 consumer_results = energy_calculator.evaluate_energy_usage() emission_results = energy_calculator.evaluate_emissions() @@ -900,8 +897,8 @@ def test_total_oil_loaded_old_method( def test_electrical_and_mechanical_power_installation( self, - ltp_test_helper, request, + ltp_test_helper, fuel_gas_factory, generator_fuel_power_to_fuel_resource, compressor_sampled_fuel_driven_resource, @@ -970,8 +967,8 @@ def test_electrical_and_mechanical_power_installation( def test_electrical_and_mechanical_power_asset( self, - ltp_test_helper, request, + ltp_test_helper, fuel_gas_factory, el_consumer_direct_base_load_factory, generator_fuel_power_to_fuel_resource, @@ -1548,3 +1545,125 @@ def test_max_usage_from_shore( 283, 283, ] + + def test_venting_emitters_direct_multiple_emissions_ltp(self, ltp_test_helper, request): + """ + Check that multiple emissions are calculated correctly for venting emitter of type DIRECT_EMISSION. + """ + time_vector = [datetime(2027, 1, 1), datetime(2028, 1, 1), datetime(2029, 1, 1)] + variables = ltp_test_helper.create_variables_map(time_vector=time_vector) + regularity = 0.2 + emission_rates = [10, 5] + + venting_emitter = ( + YamlVentingEmitterDirectTypeBuilder() + .with_name("Venting emitter 1") + .with_category(ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE) + .with_emission_names_rates_units_and_types( + names=["co2", "ch4"], + rates=emission_rates, + units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], + rate_types=[RateType.STREAM_DAY, RateType.STREAM_DAY], + ) + ).validate() + + installation = ( + YamlInstallationBuilder() + .with_name("Installation 1") + .with_category(InstallationUserDefinedCategoryType.FIXED) + .with_venting_emitters([venting_emitter]) + .with_regularity(regularity) + ).validate() + + asset = ( + YamlAssetBuilder().with_installations([installation]).with_start(time_vector[0]).with_end(time_vector[-1]) + ).validate() + + asset = ltp_test_helper.get_yaml_model(request, asset=asset, resources={}, frequency=Frequency.YEAR) + + delta_days = [ + (time_j - time_i).days for time_i, time_j in zip(variables.time_vector[:-1], variables.time_vector[1:]) + ] + + ltp_result = ltp_test_helper.get_ltp_result(asset, variables) + + ch4_emissions = ltp_test_helper.get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="co2VentingMass") + co2_emissions = ltp_test_helper.get_sum_ltp_column( + ltp_result, installation_nr=0, ltp_column="coldVentAndFugitivesCh4Mass" + ) + + assert ch4_emissions == sum(emission_rates[0] * days * regularity / 1000 for days in delta_days) + assert co2_emissions == sum(emission_rates[1] * days * regularity / 1000 for days in delta_days) + + def test_venting_emitters_volume_multiple_emissions_ltp(self, ltp_test_helper, request): + """ + Check that multiple emissions are calculated correctly for venting emitter of type OIL_VOLUME. + """ + time_vector = [datetime(2027, 1, 1), datetime(2028, 1, 1), datetime(2029, 1, 1)] + variables = ltp_test_helper.create_variables_map(time_vector=time_vector) + + regularity = 0.2 + emission_factors = [0.1, 0.1] + oil_rate = 100 + + venting_emitter = ( + YamlVentingEmitterOilTypeBuilder() + .with_name("Venting emitter 1") + .with_category(ConsumerUserDefinedCategoryType.LOADING) + .with_rate_and_emission_names_and_factors( + rate=oil_rate, + names=["ch4", "nmvoc"], + factors=emission_factors, + unit=YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, + rate_type=RateType.CALENDAR_DAY, + ) + ).validate() + + installation = ( + YamlInstallationBuilder() + .with_name("Installation calendar day") + .with_category(InstallationUserDefinedCategoryType.FIXED) + .with_venting_emitters([venting_emitter]) + .with_regularity(regularity) + ).validate() + + asset = ( + YamlAssetBuilder().with_installations([installation]).with_start(time_vector[0]).with_end(time_vector[-1]) + ).validate() + + venting_emitter_sd = deepcopy(venting_emitter) + venting_emitter_sd.volume.rate.type = RateType.STREAM_DAY + + installation_sd = deepcopy(installation) + installation_sd.venting_emitters = [venting_emitter_sd] + + asset_sd = deepcopy(asset) + asset_sd.installations = [installation_sd] + + asset = ltp_test_helper.get_yaml_model(request, asset=asset, resources={}, frequency=Frequency.YEAR) + asset_sd = ltp_test_helper.get_yaml_model(request, asset=asset_sd, resources={}, frequency=Frequency.YEAR) + + delta_days = [ + (time_j - time_i).days for time_i, time_j in zip(variables.time_vector[:-1], variables.time_vector[1:]) + ] + + ltp_result = ltp_test_helper.get_ltp_result(asset, variables) + + ltp_result_stream_day = ltp_test_helper.get_ltp_result(asset_sd, variables) + + ch4_emissions = ltp_test_helper.get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadingNmvocMass") + nmvoc_emissions = ltp_test_helper.get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadingCh4Mass") + oil_volume = ltp_test_helper.get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadedAndStoredOil") + + oil_volume_stream_day = ltp_test_helper.get_sum_ltp_column( + ltp_result_stream_day, installation_nr=0, ltp_column="loadedAndStoredOil" + ) + + assert ch4_emissions == sum(oil_rate * days * emission_factors[0] / 1000 for days in delta_days) + assert nmvoc_emissions == sum(oil_rate * days * emission_factors[1] / 1000 for days in delta_days) + assert oil_volume == pytest.approx(sum(oil_rate * days for days in delta_days), abs=1e-5) + + # Check that oil volume is including regularity correctly: + # Oil volume (input rate in stream day) / oil volume (input rates calendar day) = regularity. + # Given that the actual rate input values are the same. + assert oil_volume_stream_day / oil_volume == regularity diff --git a/tests/libecalc/presentation/yaml/test_venting_emitter.py b/tests/libecalc/presentation/yaml/test_venting_emitter.py index d59b3b33b7..5e681651f0 100644 --- a/tests/libecalc/presentation/yaml/test_venting_emitter.py +++ b/tests/libecalc/presentation/yaml/test_venting_emitter.py @@ -1,5 +1,4 @@ from datetime import datetime -from pathlib import Path import pytest @@ -10,9 +9,6 @@ from libecalc.core.result.emission import EmissionResult from libecalc.dto.types import ConsumerUserDefinedCategoryType from libecalc.expression import Expression -from libecalc.fixtures.cases import venting_emitters -from libecalc.fixtures.cases.ltp_export.utilities import get_consumption, get_sum_ltp_column -from libecalc.fixtures.cases.venting_emitters.venting_emitter_yaml import venting_emitter_yaml_factory from libecalc.presentation.yaml.yaml_types.emitters.yaml_venting_emitter import ( YamlDirectTypeEmitter, YamlOilTypeEmitter, @@ -27,299 +23,209 @@ YamlOilRateUnits, YamlOilVolumeRate, ) +from libecalc.testing.yaml_builder import ( + YamlVentingEmitterDirectTypeBuilder, + YamlInstallationBuilder, + YamlVentingEmitterOilTypeBuilder, +) -def oil_values(): - return [10, 10, 10, 10] - - -def methane(): - return [0.005, 1.5, 3, 4] - +class VentingEmitterTestHelper: + def variables_map(self): + return VariablesMap( + variables={"TSC1;Methane_rate": self.methane, "TSC1;Oil_rate": self.oil_values}, + time_vector=[ + datetime(2000, 1, 1), + datetime(2001, 1, 1), + datetime(2002, 1, 1), + datetime(2003, 1, 1), + datetime(2004, 1, 1), + ], + ) -@pytest.fixture -def variables_map(): - return VariablesMap( - variables={"TSC1;Methane_rate": methane(), "TSC1;Oil_rate": oil_values()}, - time_vector=[ - datetime(2000, 1, 1), - datetime(2001, 1, 1), - datetime(2002, 1, 1), - datetime(2003, 1, 1), - datetime(2004, 1, 1), - ], - ) + @property + def oil_values(self): + return [10, 10, 10, 10] + @property + def methane(self): + return [0.005, 1.5, 3, 4] -def test_venting_emitter(variables_map): - emitter_name = "venting_emitter" - venting_emitter = YamlDirectTypeEmitter( - name=emitter_name, - category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, - type=YamlVentingType.DIRECT_EMISSION.value, - emissions=[ - YamlVentingEmission( - name="ch4", - rate=YamlEmissionRate( - value="TSC1;Methane_rate {*} 1.02", - unit=YamlEmissionRateUnits.KILO_PER_DAY, - type=RateType.STREAM_DAY, - ), - ) - ], - ) +@pytest.fixture(scope="module") +def venting_emitter_test_helper(): + return VentingEmitterTestHelper() - regularity = {datetime(1900, 1, 1): Expression.setup_from_expression(1)} - emission_rate = venting_emitter.get_emissions(expression_evaluator=variables_map, regularity=regularity)[ - "ch4" - ].to_unit(Unit.TONS_PER_DAY) +class TestVentingEmitter: + def test_venting_emitter(self, venting_emitter_test_helper): + variables = venting_emitter_test_helper.variables_map() + emitter_name = "venting_emitter" - emission_result = { - venting_emitter.emissions[0].name: EmissionResult( - name=venting_emitter.emissions[0].name, - periods=variables_map.periods, - rate=emission_rate, - ) - } - emissions_ch4 = emission_result["ch4"] - - # Two first time steps using emitter_emission_function - assert emissions_ch4.rate.values == pytest.approx([5.1e-06, 0.00153, 0.00306, 0.00408]) - - -def test_venting_emitter_oil_volume(variables_map): - """ - Check that emissions related to oil loading/storage are correct. These emissions are - calculated using a factor of input oil rates, i.e. the TYPE set to OIL_VOLUME. - """ - emitter_name = "venting_emitter" - emission_factor = 0.1 - regularity_expected = 1.0 - - venting_emitter = YamlOilTypeEmitter( - name=emitter_name, - category=ConsumerUserDefinedCategoryType.LOADING, - type=YamlVentingType.OIL_VOLUME.value, - volume=YamlVentingVolume( - rate=YamlOilVolumeRate( - value="TSC1;Oil_rate", - unit=YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, - type=RateType.STREAM_DAY, - ), + venting_emitter = YamlDirectTypeEmitter( + name=emitter_name, + category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, + type=YamlVentingType.DIRECT_EMISSION.value, emissions=[ - YamlVentingVolumeEmission( + YamlVentingEmission( name="ch4", - emission_factor=emission_factor, + rate=YamlEmissionRate( + value="TSC1;Methane_rate {*} 1.02", + unit=YamlEmissionRateUnits.KILO_PER_DAY, + type=RateType.STREAM_DAY, + ), ) ], - ), - ) - - regularity = {Period(datetime(1900, 1, 1)): Expression.setup_from_expression(regularity_expected)} + ) - emission_rate = venting_emitter.get_emissions(expression_evaluator=variables_map, regularity=regularity)[ - "ch4" - ].to_unit(Unit.TONS_PER_DAY) + regularity = {datetime(1900, 1, 1): Expression.setup_from_expression(1)} - emission_result = { - venting_emitter.volume.emissions[0].name: EmissionResult( - name=venting_emitter.volume.emissions[0].name, - periods=variables_map.periods, - rate=emission_rate, - ) - } - emissions_ch4 = emission_result["ch4"] + emission_rate = venting_emitter.get_emissions(expression_evaluator=variables, regularity=regularity)[ + "ch4" + ].to_unit(Unit.TONS_PER_DAY) - expected_result = [oil_value * regularity_expected * emission_factor / 1000 for oil_value in oil_values()] + emission_result = { + venting_emitter.emissions[0].name: EmissionResult( + name=venting_emitter.emissions[0].name, + periods=variables.periods, + rate=emission_rate, + ) + } + emissions_ch4 = emission_result["ch4"] - assert emissions_ch4.rate.values == expected_result + # Two first time steps using emitter_emission_function + assert emissions_ch4.rate.values == pytest.approx([5.1e-06, 0.00153, 0.00306, 0.00408]) + def test_venting_emitter_oil_volume(self, venting_emitter_test_helper): + """ + Check that emissions related to oil loading/storage are correct. These emissions are + calculated using a factor of input oil rates, i.e. the TYPE set to OIL_VOLUME. + """ + emitter_name = "venting_emitter" + emission_factor = 0.1 + regularity_expected = 1.0 -def test_no_emissions_direct(variables_map): - """ - Check that error message is given if no emissions are specified for TYPE DIRECT_EMISSION. - """ - emitter_name = "venting_emitter" + variables = venting_emitter_test_helper.variables_map() - with pytest.raises(ValueError) as exc: - YamlDirectTypeEmitter( + venting_emitter = YamlOilTypeEmitter( name=emitter_name, - category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, - type=YamlVentingType.DIRECT_EMISSION.name, + category=ConsumerUserDefinedCategoryType.LOADING, + type=YamlVentingType.OIL_VOLUME.value, + volume=YamlVentingVolume( + rate=YamlOilVolumeRate( + value="TSC1;Oil_rate", + unit=YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, + type=RateType.STREAM_DAY, + ), + emissions=[ + YamlVentingVolumeEmission( + name="ch4", + emission_factor=emission_factor, + ) + ], + ), ) - assert "1 validation error for VentingEmitter\nEMISSIONS\n Field required" in str(exc.value) + regularity = {Period(datetime(1900, 1, 1)): Expression.setup_from_expression(regularity_expected)} + + emission_rate = venting_emitter.get_emissions(expression_evaluator=variables, regularity=regularity)[ + "ch4" + ].to_unit(Unit.TONS_PER_DAY) + + emission_result = { + venting_emitter.volume.emissions[0].name: EmissionResult( + name=venting_emitter.volume.emissions[0].name, + periods=variables.periods, + rate=emission_rate, + ) + } + emissions_ch4 = emission_result["ch4"] + + expected_result = [ + oil_value * regularity_expected * emission_factor / 1000 + for oil_value in venting_emitter_test_helper.oil_values + ] + + assert emissions_ch4.rate.values == expected_result + + def test_no_emissions_direct(self): + """ + Check that error message is given if no emissions are specified for TYPE DIRECT_EMISSION. + """ + + with pytest.raises(ValueError) as exc: + YamlDirectTypeEmitter( + name="venting_emitter", + category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, + type=YamlVentingType.DIRECT_EMISSION.name, + ) + assert "1 validation error for VentingEmitter\nEMISSIONS\n Field required" in str(exc.value) -def test_no_volume_oil(variables_map): - """ - Check that error message is given if no volume is specified for TYPE OIL_VOLUME. - """ - emitter_name = "venting_emitter" + def test_no_volume_oil(self): + """ + Check that error message is given if no volume is specified for TYPE OIL_VOLUME. + """ - with pytest.raises(ValueError) as exc: - YamlOilTypeEmitter( - name=emitter_name, - category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, - type=YamlVentingType.OIL_VOLUME.name, - ) + with pytest.raises(ValueError) as exc: + YamlOilTypeEmitter( + name="venting_emitter", + category=ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE, + type=YamlVentingType.OIL_VOLUME.name, + ) + + assert "1 validation error for VentingEmitter\nVOLUME\n Field required" in str(exc.value) + + def test_venting_emitters_direct_uppercase_emissions_name(self): + """ + Check emission names are case-insensitive for venting emitters of type DIRECT_EMISSION. + """ + + emission_rates = [10, 5] + + venting_emitter = ( + YamlVentingEmitterDirectTypeBuilder() + .with_name("Venting emitter 1") + .with_category(ConsumerUserDefinedCategoryType.COLD_VENTING_FUGITIVE) + .with_emission_names_rates_units_and_types( + names=["CO2", "nmVOC"], + rates=emission_rates, + units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], + rate_types=[RateType.STREAM_DAY, RateType.STREAM_DAY], + ) + ).validate() + + installation = ( + YamlInstallationBuilder().with_name("Installation").with_venting_emitters([venting_emitter]) + ).validate() + + assert installation.venting_emitters[0].emissions[0].name == "co2" + assert installation.venting_emitters[0].emissions[1].name == "nmvoc" + + def test_venting_emitters_volume_uppercase_emissions_name(self): + """ + Check emission names are case-insensitive for venting emitters of type OIL_VOLUME. + """ + + emission_factors = [0.1, 0.1] + oil_rate = 100 + + venting_emitter = ( + YamlVentingEmitterOilTypeBuilder() + .with_name("Venting emitter 1") + .with_category(ConsumerUserDefinedCategoryType.LOADING) + .with_rate_and_emission_names_and_factors( + rate=oil_rate, + names=["CO2", "nmVOC"], + factors=emission_factors, + unit=YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, + rate_type=RateType.CALENDAR_DAY, + ) + ).validate() + + installation = ( + YamlInstallationBuilder().with_name("Installation").with_venting_emitters([venting_emitter]) + ).validate() - assert "1 validation error for VentingEmitter\nVOLUME\n Field required" in str(exc.value) - - -def test_venting_emitters_direct_multiple_emissions_ltp(): - """ - Check that multiple emissions are calculated correctly for venting emitter of type DIRECT_EMISSION. - """ - - regularity = 0.2 - emission_rates = [10, 5] - dto_case = venting_emitter_yaml_factory( - emission_rates=emission_rates, - regularity=regularity, - units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], - emission_names=["co2", "ch4"], - rate_types=[RateType.STREAM_DAY], - emission_keyword_name="EMISSIONS", - categories=["COLD-VENTING-FUGITIVE"], - names=["Venting emitter 1"], - path=Path(venting_emitters.__path__[0]), - ) - - delta_days = [ - (time_j - time_i).days - for time_i, time_j in zip(dto_case.variables.time_vector[:-1], dto_case.variables.time_vector[1:]) - ] - - ltp_result = get_consumption( - model=dto_case.ecalc_model, variables=dto_case.variables, periods=dto_case.variables.get_periods() - ) - - ch4_emissions = get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="co2VentingMass") - co2_emissions = get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="coldVentAndFugitivesCh4Mass") - - assert ch4_emissions == sum(emission_rates[0] * days * regularity / 1000 for days in delta_days) - assert co2_emissions == sum(emission_rates[1] * days * regularity / 1000 for days in delta_days) - - -def test_venting_emitters_volume_multiple_emissions_ltp(): - """ - Check that multiple emissions are calculated correctly for venting emitter of type OIL_VOLUME. - """ - - regularity = 0.2 - emission_factors = [0.1, 0.1] - oil_rates = [100] - - path = Path(venting_emitters.__path__[0]) - dto_case = venting_emitter_yaml_factory( - regularity=regularity, - units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], - units_oil_rates=[YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY], - emission_names=["ch4", "nmvoc"], - emitter_types=["OIL_VOLUME"], - rate_types=[RateType.CALENDAR_DAY], - categories=["LOADING"], - names=["Venting emitter 1"], - emission_factors=emission_factors, - oil_rates=oil_rates, - path=path, - ) - - dto_case_stream_day = venting_emitter_yaml_factory( - regularity=regularity, - units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], - units_oil_rates=[YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY], - emission_names=["ch4", "nmvoc"], - emitter_types=["OIL_VOLUME"], - rate_types=[RateType.STREAM_DAY], - categories=["LOADING"], - names=["Venting emitter 1"], - emission_factors=emission_factors, - oil_rates=oil_rates, - path=path, - ) - - delta_days = [ - (time_j - time_i).days - for time_i, time_j in zip(dto_case.variables.time_vector[:-1], dto_case.variables.time_vector[1:]) - ] - - ltp_result = get_consumption( - model=dto_case.ecalc_model, variables=dto_case.variables, periods=dto_case.variables.periods - ) - - ltp_result_stream_day = get_consumption( - model=dto_case_stream_day.ecalc_model, - variables=dto_case_stream_day.variables, - periods=dto_case_stream_day.variables.periods, - ) - - ch4_emissions = get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadingNmvocMass") - nmvoc_emissions = get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadingCh4Mass") - oil_volume = get_sum_ltp_column(ltp_result, installation_nr=0, ltp_column="loadedAndStoredOil") - - oil_volume_stream_day = get_sum_ltp_column( - ltp_result_stream_day, installation_nr=0, ltp_column="loadedAndStoredOil" - ) - - assert ch4_emissions == sum(oil_rates[0] * days * emission_factors[0] / 1000 for days in delta_days) - assert nmvoc_emissions == sum(oil_rates[0] * days * emission_factors[1] / 1000 for days in delta_days) - assert oil_volume == pytest.approx(sum(oil_rates[0] * days for days in delta_days), abs=1e-5) - - # Check that oil volume is including regularity correctly: - # Oil volume (input rate in stream day) / oil volume (input rates calendar day) = regularity. - # Given that the actual rate input values are the same. - assert oil_volume_stream_day / oil_volume == regularity - - -def test_venting_emitters_direct_uppercase_emissions_name(): - """ - Check emission names are case-insensitive for venting emitters of type DIRECT_EMISSION. - """ - - regularity = 0.2 - emission_rates = [10, 5] - dto_case = venting_emitter_yaml_factory( - emission_rates=emission_rates, - regularity=regularity, - units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], - emission_names=["CO2", "nmVOC"], - rate_types=[RateType.STREAM_DAY], - emission_keyword_name="EMISSIONS", - categories=["COLD-VENTING-FUGITIVE"], - names=["Venting emitter 1"], - path=Path(venting_emitters.__path__[0]), - ) - - assert dto_case.ecalc_model.installations[0].venting_emitters[0].emissions[0].name == "co2" - assert dto_case.ecalc_model.installations[0].venting_emitters[0].emissions[1].name == "nmvoc" - - -def test_venting_emitters_volume_uppercase_emissions_name(): - """ - Check emission names are case-insensitive for venting emitters of type OIL_VOLUME. - """ - - regularity = 0.2 - emission_factors = [0.1, 0.1] - oil_rates = [100] - - dto_case = venting_emitter_yaml_factory( - regularity=regularity, - units=[YamlEmissionRateUnits.KILO_PER_DAY, YamlEmissionRateUnits.KILO_PER_DAY], - units_oil_rates=[YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY, YamlOilRateUnits.STANDARD_CUBIC_METER_PER_DAY], - emission_names=["CO2", "nmVOC"], - emitter_types=["OIL_VOLUME"], - rate_types=[RateType.CALENDAR_DAY], - categories=["LOADING"], - names=["Venting emitter 1"], - emission_factors=emission_factors, - oil_rates=oil_rates, - path=Path(venting_emitters.__path__[0]), - ) - - assert dto_case.ecalc_model.installations[0].venting_emitters[0].volume.emissions[0].name == "co2" - assert dto_case.ecalc_model.installations[0].venting_emitters[0].volume.emissions[1].name == "nmvoc" + assert installation.venting_emitters[0].volume.emissions[0].name == "co2" + assert installation.venting_emitters[0].volume.emissions[1].name == "nmvoc"