Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add default CI to Fuel Category #1490

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading