Skip to content

Commit

Permalink
feat: Add default CI to Fuel Category
Browse files Browse the repository at this point in the history
* Default CI is actually associated to fuel category and used as a fallback in several scenarios
* The one covered here is Other, where if the user selects other it needs to use the CI of the Cateory, NOT the Type.
  • Loading branch information
dhaselhan committed Dec 18, 2024
1 parent bb9e0e6 commit f6a18d6
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Add Default CI to Categories
Revision ID: 851e09cf8661
Revises: 5b374dd97469
Create Date: 2024-12-17 23:58:07.462215
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "851e09cf8661"
down_revision = "5b374dd97469"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"fuel_category",
sa.Column(
"default_carbon_intensity",
sa.Numeric(precision=10, scale=2),
nullable=True,
comment="Default carbon intensity of the fuel category",
),
)

# Populate default values for existing records
op.execute(
"""
UPDATE "fuel_category" SET "default_carbon_intensity" = 88.83 WHERE "description" = 'Jet fuel';
"""
)
op.execute(
"""
UPDATE "fuel_category" SET "default_carbon_intensity" = 100.21 WHERE "description" = 'Diesel';
"""
)
op.execute(
"""
UPDATE "fuel_category" SET "default_carbon_intensity" = 93.67 WHERE "description" = 'Gasoline';
"""
)

# Now set the column to NOT NULL after populating defaults
op.alter_column(
"fuel_category",
"default_carbon_intensity",
existing_type=sa.Numeric(precision=10, scale=2),
nullable=False,
)

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("fuel_category", "default_carbon_intensity")
# ### end Alembic commands ###
11 changes: 9 additions & 2 deletions backend/lcfs/db/models/fuel/FuelCategory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, Text, Enum
from sqlalchemy import Column, Integer, Text, Enum, Float, Numeric
from lcfs.db.base import BaseModel, Auditable, DisplayOrder, EffectiveDates
from sqlalchemy.orm import relationship

Expand All @@ -25,7 +25,14 @@ class FuelCategory(BaseModel, Auditable, DisplayOrder, EffectiveDates):
nullable=False,
comment="Name of the fuel category",
)
description = Column(Text, nullable=True, comment="Description of the fuel categor")
description = Column(
Text, nullable=True, comment="Description of the fuel category"
)
default_carbon_intensity = Column(
Numeric(10, 2),
nullable=False,
comment="Default carbon intensity of the fuel category",
)

energy_effectiveness_ratio = relationship("EnergyEffectivenessRatio")
target_carbon_intensities = relationship(
Expand Down
11 changes: 7 additions & 4 deletions backend/lcfs/db/seeders/common/seed_fuel_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,17 +249,20 @@
{
"fuel_category_id": 1,
"category": "Gasoline",
"description": "Gasoline"
"description": "Gasoline",
"default_carbon_intensity": 93.67
},
{
"fuel_category_id": 2,
"category": "Diesel",
"description": "Diesel"
"description": "Diesel",
"default_carbon_intensity": 100.21
},
{
"fuel_category_id": 3,
"category": "Jet fuel",
"description": "Jet fuel"
"description": "Jet fuel",
"default_carbon_intensity": 88.83
}
],
"end_use_types": [
Expand Down Expand Up @@ -1092,4 +1095,4 @@
"display_order": 4
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ async def expected_uses(dbsession):
@pytest.fixture
async def fuel_categories(dbsession):
fuel_categories = [
FuelCategory(fuel_category_id=998, category="Gasoline"),
FuelCategory(fuel_category_id=999, category="Diesel"),
FuelCategory(
fuel_category_id=998, category="Gasoline", default_carbon_intensity=0
),
FuelCategory(
fuel_category_id=999, category="Diesel", default_carbon_intensity=0
),
]

dbsession.add_all(fuel_categories)
Expand Down
92 changes: 89 additions & 3 deletions backend/lcfs/tests/fuel_code/test_fuel_code_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ async def test_get_fuel_type_by_id_not_found(fuel_code_repo, mock_db):

@pytest.mark.anyio
async def test_get_fuel_categories(fuel_code_repo, mock_db):
mock_fc = FuelCategory(fuel_category_id=1, category="Renewable")
mock_fc = FuelCategory(
fuel_category_id=1, category="Renewable", default_carbon_intensity=0
)
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = [mock_fc]
mock_db.execute.return_value = mock_result
Expand All @@ -157,12 +159,14 @@ async def test_get_fuel_categories(fuel_code_repo, mock_db):

@pytest.mark.anyio
async def test_get_fuel_category_by_name(fuel_code_repo, mock_db):
mock_fc = FuelCategory(fuel_category_id=2, category="Fossil")
mock_fc = FuelCategory(
fuel_category_id=2, category="Fossil", default_carbon_intensity=0
)
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = mock_fc
mock_db.execute.return_value = mock_result

result = await fuel_code_repo.get_fuel_category_by_name("Fossil")
result = await fuel_code_repo.get_fuel_category_by(category="Fossil")
assert result == mock_fc


Expand Down Expand Up @@ -646,6 +650,88 @@ async def test_get_standardized_fuel_data(fuel_code_repo, mock_db):
assert result.uci == 5.0


@pytest.mark.anyio
async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db):
# Mock an unrecognized fuel type
mock_fuel_type = FuelType(
fuel_type_id=1,
fuel_type="UnknownFuel",
default_carbon_intensity=None,
unrecognized=True,
)

# Mock a fuel category with a default CI
mock_fuel_category = FuelCategory(
fuel_category_id=2, category="SomeCategory", default_carbon_intensity=93.67
)

# The repo uses get_one to get the fuel type.
mock_db.get_one.return_value = mock_fuel_type

# Mock the repo method to get the fuel category
fuel_code_repo.get_fuel_category_by = AsyncMock(return_value=mock_fuel_category)

# Setup side effects for subsequent queries:
# Energy Density
energy_density_result = MagicMock(
scalars=MagicMock(
return_value=MagicMock(
first=MagicMock(return_value=EnergyDensity(density=35.0))
)
)
)
# EER
eer_result = MagicMock(
scalars=MagicMock(
return_value=MagicMock(
first=MagicMock(return_value=EnergyEffectivenessRatio(ratio=2.0))
)
)
)
# Target Carbon Intensities
tci_result = MagicMock(
scalars=MagicMock(
return_value=MagicMock(
all=MagicMock(
return_value=[TargetCarbonIntensity(target_carbon_intensity=50.0)]
)
)
)
)
# Additional Carbon Intensity
aci_result = MagicMock(
scalars=MagicMock(
return_value=MagicMock(
one_or_none=MagicMock(
return_value=AdditionalCarbonIntensity(intensity=5.0)
)
)
)
)

# Set the side_effect for the mock_db.execute calls in the order they're invoked
mock_db.execute.side_effect = [
energy_density_result,
eer_result,
tci_result,
aci_result,
]

result = await fuel_code_repo.get_standardized_fuel_data(
fuel_type_id=1, fuel_category_id=2, end_use_id=3, compliance_period="2024"
)

# Since fuel_type is unrecognized, it should use the FuelCategory's default CI
assert result.effective_carbon_intensity == 93.67
assert result.target_ci == 50.0
assert result.eer == 2.0
assert result.energy_density == 35.0
assert result.uci == 5.0

# Ensure get_fuel_category_by was called once with the correct parameter
fuel_code_repo.get_fuel_category_by.assert_awaited_once_with(fuel_category_id=2)


@pytest.mark.anyio
async def test_get_additional_carbon_intensity(fuel_code_repo, mock_db):
aci = AdditionalCarbonIntensity(additional_uci_id=1, intensity=10.0)
Expand Down
8 changes: 4 additions & 4 deletions backend/lcfs/web/api/allocation_agreement/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ async def convert_to_model(
allocation_agreement.allocation_transaction_type
)
)
fuel_category = await self.fuel_repo.get_fuel_category_by_name(
allocation_agreement.fuel_category
fuel_category = await self.fuel_repo.get_fuel_category_by(
category=allocation_agreement.fuel_category
)
fuel_type = await self.fuel_repo.get_fuel_type_by_name(
allocation_agreement.fuel_type
Expand Down Expand Up @@ -226,8 +226,8 @@ async def update_allocation_agreement(
!= allocation_agreement_data.fuel_category
):
existing_allocation_agreement.fuel_category = (
await self.fuel_repo.get_fuel_category_by_name(
allocation_agreement_data.fuel_category
await self.fuel_repo.get_fuel_category_by(
category=allocation_agreement_data.fuel_category
)
)

Expand Down
15 changes: 10 additions & 5 deletions backend/lcfs/web/api/fuel_code/repo.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from dataclasses import dataclass
from datetime import date
from typing import List, Dict, Any, Union, Optional, Sequence

import structlog
from fastapi import Depends
from sqlalchemy import and_, or_, select, func, text, update, distinct, desc, asc
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, contains_eager

Expand Down Expand Up @@ -33,7 +33,6 @@
)
from lcfs.web.api.fuel_code.schema import FuelCodeCloneSchema, FuelCodeSchema
from lcfs.web.core.decorators import repo_handler
from dataclasses import dataclass

logger = structlog.get_logger(__name__)

Expand Down Expand Up @@ -175,9 +174,9 @@ async def get_fuel_categories(self) -> List[FuelCategory]:
return (await self.db.execute(select(FuelCategory))).scalars().all()

@repo_handler
async def get_fuel_category_by_name(self, name: str) -> FuelCategory:
"""Get a fuel category by its name"""
result = await self.db.execute(select(FuelCategory).filter_by(category=name))
async def get_fuel_category_by(self, **filters: Any) -> FuelCategory:
"""Get a fuel category by any filters"""
result = await self.db.execute(select(FuelCategory).filter_by(**filters))
return result.scalar_one_or_none()

@repo_handler
Expand Down Expand Up @@ -861,6 +860,12 @@ async def get_standardized_fuel_data(
if fuel_code_id:
fuel_code = await self.get_fuel_code(fuel_code_id)
effective_carbon_intensity = fuel_code.carbon_intensity
# Other Fuel uses the Default CI of the Category
elif fuel_type.unrecognized:
fuel_category = await self.get_fuel_category_by(
fuel_category_id=fuel_category_id
)
effective_carbon_intensity = fuel_category.default_carbon_intensity
else:
effective_carbon_intensity = fuel_type.default_carbon_intensity

Expand Down

0 comments on commit f6a18d6

Please sign in to comment.