diff --git a/README.md b/README.md index 6ffa57a..75e3589 100644 --- a/README.md +++ b/README.md @@ -59,17 +59,17 @@ python scripts/verify_installation.py ### 🖥️ **Running the Desktop Application** ```sh -python src/osbridgelcca/desktop/app.py +python src_web/osbridgelcca/desktop/app.py ``` ### 🌐 **Running the Web Application** #### **Start the Backend Server** ```sh -python src/osbridgelcca/backend/manage.py runserver +python src_web/osbridgelcca/backend/manage.py runserver ``` #### **Start the Frontend** ```sh -cd src/osbridgelcca/web +cd src_web/osbridgelcca/web npm install npm start ``` diff --git a/install.bat b/install.bat index 5a1a326..312ff7a 100644 --- a/install.bat +++ b/install.bat @@ -1,6 +1,13 @@ @echo off echo Creating and activating Conda environment... -call conda activate osbridgelcca || conda create -n osbridgelcca python=3.9 -y && conda activate osbridgelcca + +REM Try activating the environment +call conda activate osbridgelcca 2>nul +if errorlevel 1 ( + echo Environment not found. Creating it now... + call conda create -n osbridgelcca python=3.9 -y + call conda activate osbridgelcca +) echo Installing dependencies from pyproject.toml... pip install . diff --git a/scripts/verify_installation.py b/scripts/verify_installation.py index 9e6afe4..8ceaa8c 100644 --- a/scripts/verify_installation.py +++ b/scripts/verify_installation.py @@ -1,5 +1,6 @@ import importlib import sys +sys.stdout.reconfigure(encoding='utf-8') # List of required packages REQUIRED_PACKAGES = [ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/osbridgelcca/backend/__init__.py b/src/osbridgelcca/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/osbridgelcca/backend/config.py b/src/osbridgelcca/backend/config.py index 5aea750..3d78f4d 100644 --- a/src/osbridgelcca/backend/config.py +++ b/src/osbridgelcca/backend/config.py @@ -1 +1,22 @@ # Placeholder for app configuration + +import os +from pathlib import Path + +# Base directory of the project +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +# Database configuration +DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{BASE_DIR}/data/lcca.db') + +# Create data directory if it doesn't exist +DATA_DIR = BASE_DIR / 'data' +DATA_DIR.mkdir(exist_ok=True) + +# Database configuration options +DB_CONFIG = { + 'pool_size': 5, + 'max_overflow': 10, + 'pool_timeout': 30, + 'pool_recycle': 1800 +} diff --git a/src/osbridgelcca/backend/database.py b/src/osbridgelcca/backend/database.py new file mode 100644 index 0000000..b239887 --- /dev/null +++ b/src/osbridgelcca/backend/database.py @@ -0,0 +1,28 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import QueuePool +from .models import Base +from .config import DATABASE_URL, DB_CONFIG + +# Create engine with connection pooling +engine = create_engine( + DATABASE_URL, + poolclass=QueuePool, + **DB_CONFIG +) + +# Create session factory +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def get_db(): + """Get database session""" + db = SessionLocal() + try: + yield db + finally: + db.close() + +def init_database(): + """Initialize database tables""" + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/src/osbridgelcca/backend/main.py b/src/osbridgelcca/backend/main.py index daa4a22..d768a4c 100644 --- a/src/osbridgelcca/backend/main.py +++ b/src/osbridgelcca/backend/main.py @@ -1 +1,3 @@ -# Placeholder for Flask API entry point +from osbridgelcca.backend.database import init_database + +init_database() diff --git a/src/osbridgelcca/backend/models.py b/src/osbridgelcca/backend/models.py new file mode 100644 index 0000000..c99a675 --- /dev/null +++ b/src/osbridgelcca/backend/models.py @@ -0,0 +1,92 @@ +from sqlalchemy import create_engine, Column, Integer, Float, String, ForeignKey, DateTime, JSON +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from datetime import datetime, UTC + +Base = declarative_base() + +class MaterialRate(Base): + """Regional schedule of rates for materials""" + __tablename__ = 'material_rates' + + id = Column(Integer, primary_key=True) + material_name = Column(String(100), nullable=False) + region = Column(String(50), nullable=False) + unit = Column(String(20), nullable=False) + rate = Column(Float, nullable=False) + currency = Column(String, nullable=False) + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class ScrapValue(Base): + """Scrap values and recyclability percentages""" + __tablename__ = 'scrap_values' + + id = Column(Integer, primary_key=True) + material_name = Column(String(100), nullable=False) + scrap_value = Column(Float, nullable=False) + recyclability_percentage = Column(Float, nullable=False) + currency = Column(String, nullable=False) + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class EnvironmentalImpact(Base): + """Environmental impact data from EPDs""" + __tablename__ = 'environmental_impacts' + + id = Column(Integer, primary_key=True) + material_name = Column(String(100), nullable=False) + epd_id = Column(String(50), nullable=False) + global_warming_potential = Column(Float, nullable=False) # kg CO2 eq + water_consumption = Column(Float) # m³ + energy_consumption = Column(Float) # MJ + waste_generation = Column(Float) # kg + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class SocialCostOfCarbon(Base): + """Country-specific social cost of carbon""" + __tablename__ = 'social_cost_of_carbon' + + id = Column(Integer, primary_key=True) + country = Column(String(50), nullable=False) + year = Column(Integer, nullable=False) + cost_per_ton = Column(Float, nullable=False) # USD per ton CO2 + currency = Column(String, nullable=False) + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class TransportEmissionFactor(Base): + """Road transport carbon emission factors""" + __tablename__ = 'transport_emission_factors' + + id = Column(Integer, primary_key=True) + vehicle_type = Column(String(50), nullable=False) + emission_factor = Column(Float, nullable=False) # kg CO2 per km + load_factor = Column(Float) # Average load factor + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class VehicleOperationCost(Base): + """Vehicle operation costs""" + __tablename__ = 'vehicle_operation_costs' + + id = Column(Integer, primary_key=True) + vehicle_type = Column(String(50), nullable=False) + fuel_cost_per_km = Column(Float, nullable=False) + maintenance_cost_per_km = Column(Float, nullable=False) + insurance_cost_per_year = Column(Float, nullable=False) + currency = Column(String(3), nullable=False) + last_updated = Column(DateTime, default=lambda: datetime.now(UTC)) + +class UserAnalysis(Base): + """User analyses storage""" + __tablename__ = 'user_analyses' + + id = Column(Integer, primary_key=True) + user_id = Column(String(50), nullable=False) + analysis_name = Column(String(100), nullable=False) + analysis_data = Column(JSON, nullable=False) + created_at = Column(DateTime, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) + +def init_db(db_url): + """Initialize the database""" + engine = create_engine(db_url) + Base.metadata.create_all(engine) + return engine \ No newline at end of file diff --git a/src/osbridgelcca/core/__init__.py b/src/osbridgelcca/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/osbridgelcca/core/bridge_lcc.py b/src/osbridgelcca/core/bridge_lcc.py index 4cb3716..1b9138c 100644 --- a/src/osbridgelcca/core/bridge_lcc.py +++ b/src/osbridgelcca/core/bridge_lcc.py @@ -1,26 +1,84 @@ +import pandas as pd +from input_data import Input +from cost_components import CostComponent +from src.osbridgelcca.core.cost_components import InitialConstructionCost, InitialCarbonEmissionCost, TimeCost, \ + RoadUserCost + + class BridgeLCC: """Main class for handling Life Cycle Cost Analysis of bridges.""" - - def __init__(self, project_name, inputs): + + def __init__(self, project_name: str, inputs: Input): self.project_name = project_name - self.inputs = inputs # Dictionary of user inputs - self.outputs = {} + self.inputs = inputs # Expecting an instance of Input class + self.outputs = {} # Initialize output storage def calculate_lcc(self): """Calculate the total life cycle cost based on input parameters.""" try: - material_cost = sum(self.inputs["bill_of_quantity"].values()) - maintenance_cost = self.inputs["maintenance_cost"] - operation_cost = self.inputs["vehicle_operating_cost"] - discount_rate = self.inputs["discount_rate"] + material_cost = self.inputs.material_data.quantity * self.inputs.material_data.unit_rate + maintenance_cost = self.inputs.maintenance_data.periodic_maintenance_rate + operation_cost = self.inputs.traffic_data.annual_traffic_increase * 5000 # Example formula + discount_rate = self.inputs.finance_data.discount_rate / 100 # Convert percentage to decimal # Simple NPV calculation total_cost = (material_cost + maintenance_cost + operation_cost) / (1 + discount_rate) self.outputs["total_lcc"] = total_cost return total_cost - except KeyError as e: + except AttributeError as e: raise ValueError(f"Missing input parameter: {e}") def get_outputs(self): """Return computed outputs.""" return self.outputs + + def tabulate_cost_components(self): + """Create a Pandas DataFrame for cost components.""" + data = { + "Sl. No.": list(range(1, 12)) + ["Total"], + "Cost Component": [ + "Initial costs", "Initial carbon emissions costs", "Time costs", + "Road user costs due to re-routing", "Carbon emission cost due to re-routing", + "Periodic maintenance costs", "Periodic maintenance carbon emissions costs", + "Annual routine inspection costs", "Repair and rehabilitation costs", + "Demolition and disposal costs", "Recycling costs", "Total life cycle cost" + ], + "Cost Amount": [InitialConstructionCost.amount, + InitialCarbonEmissionCost.amount, + TimeCost.amount, + RoadUserCost.amount, + 82.62, 19.36, 1.5, 17.68, 13.8, 0, 0.84, -2.67, 212.81], + "Life Cycle Stage": [ + "Initial", "Initial", "Initial", "Initial", "Initial", + "Use", "Use", "Use", "Use", + "End", "End", "Total" + ], + "Cost Type": [ + "Economic", "Environmental", "Social", "Social", "Environmental", + "Economic", "Environmental", "Economic", "Economic", + "Economic", "Economic", "Total" + ] + } + + df = pd.DataFrame(data) + return df + +# Example Usage +if __name__ == "__main__": + from input_data import MaterialData, FinanceData, TrafficData, MaintenanceData, RepairData, DemolitionData, RecycleData, CarbonEmissionData + + material = MaterialData("Steel", "Reinforcement bars", 5000, "kg", 60, "Govt Schedule", 32, 2.5, "EPD Source", 5, 90) + finance = FinanceData(5.0, 7.5, 1.2) + carbon = CarbonEmissionData("SSP2", "RCP6", 1200) + traffic = TrafficData(15, 4000, "Plain", "Urban", 10, {"Car": 50, "Truck": 30, "Bus": 20}) + maintenance = MaintenanceData(0.55, 1.0, 10, 5, 1) + repair = RepairData("Component-wise repair details") + demolition = DemolitionData(10) + recycle = RecycleData(80, 10000, 5000) + + project_input = Input(material, finance, carbon, traffic, maintenance, repair, demolition, recycle) + bridge = BridgeLCC("Bridge A", project_input) + + print("Total LCC:", bridge.calculate_lcc()) + print("\nCost Components Table:") + print(bridge.tabulate_cost_components()) diff --git a/src/osbridgelcca/core/cost_component.py b/src/osbridgelcca/core/cost_component.py deleted file mode 100644 index c839b93..0000000 --- a/src/osbridgelcca/core/cost_component.py +++ /dev/null @@ -1,176 +0,0 @@ -from abc import ABC, abstractmethod - -class CostComponent(ABC): - """Abstract Base Class for different cost components in Life Cycle Cost Analysis.""" - - def __init__(self, amount, category, is_initial, is_recurring, present_worth_factor): - """ - Initialize a generic cost component. - - :param amount: Cost amount in INR - :param category: Economic, Environmental, or Social - :param is_initial: True if an initial cost, False if future cost - :param is_recurring: True if recurring, False if one-time - :param present_worth_factor: Discounting factor for future costs (present_worth_factor) - """ - self.amount = amount - self.category = category - self.is_initial = is_initial - self.is_recurring = is_recurring - self.present_worth_factor = present_worth_factor - - @abstractmethod - def calculate_cost(self): - """Abstract method to be implemented by subclasses for cost calculation.""" - pass - - -class InitialConstructionCost(CostComponent): - """Covers material, labor, and equipment costs for bridge construction.""" - - def __init__(self, quantity, rate): - super().__init__(amount=quantity * rate, category="Economic", is_initial=True, is_recurring=False, present_worth_factor=1.00) - self.quantity = quantity - self.rate = rate - - def calculate_cost(self): - return self.quantity * self.rate * self.present_worth_factor - - -class InitialCarbonEmissionCost(CostComponent): - """Calculates initial carbon emissions from material production and transport.""" - - def __init__(self, material_quantity, carbon_emission_factor, carbon_cost): - super().__init__(amount=(material_quantity * carbon_emission_factor) * carbon_cost, category="Environmental", is_initial=True, is_recurring=False, present_worth_factor=1.00) - self.material_quantity = material_quantity - self.carbon_emission_factor = carbon_emission_factor - self.carbon_cost = carbon_cost - - def calculate_cost(self): - return (self.material_quantity * self.carbon_emission_factor) * self.carbon_cost * self.present_worth_factor - - -class TimeCost(CostComponent): - """Calculates economic losses due to construction delays.""" - - def __init__(self, construction_cost, interest_rate, time, investment_ratio): - cost = construction_cost * interest_rate * time * investment_ratio - super().__init__(amount=cost, category="Economic", is_initial=True, is_recurring=False, present_worth_factor=1.00) - self.construction_cost = construction_cost - self.interest_rate = interest_rate - self.time = time - self.investment_ratio = investment_ratio - - def calculate_cost(self): - return self.construction_cost * self.interest_rate * self.time * self.investment_ratio * self.present_worth_factor - - -class RoadUserCost(CostComponent): - """Evaluates economic impact on road users due to delays and detours.""" - - def __init__(self, vehicles_affected, vehicle_operation_cost, construction_time): - cost = vehicles_affected * vehicle_operation_cost * construction_time - super().__init__(amount=cost, category="Economic", is_initial=True, is_recurring=False, present_worth_factor=1.00) - self.vehicles_affected = vehicles_affected - self.vehicle_operation_cost = vehicle_operation_cost - self.construction_time = construction_time - - def calculate_cost(self): - return self.vehicles_affected * self.vehicle_operation_cost * self.construction_time * self.present_worth_factor - - -class AdditionalCarbonEmissionCost(CostComponent): - """Accounts for increased emissions from detoured traffic during bridge work.""" - - def __init__(self, vehicles_affected, reroute_distance, co2_emission_per_km, carbon_cost): - cost = vehicles_affected * reroute_distance * co2_emission_per_km * carbon_cost - super().__init__(amount=cost, category="Environmental", is_initial=True, is_recurring=False, present_worth_factor=1.00) - self.vehicles_affected = vehicles_affected - self.reroute_distance = reroute_distance - self.co2_emission_per_km = co2_emission_per_km - self.carbon_cost = carbon_cost - - def calculate_cost(self): - return self.vehicles_affected * self.reroute_distance * self.co2_emission_per_km * self.carbon_cost * self.present_worth_factor - - -class PeriodicMaintenanceCost(CostComponent): - """Includes expenses for routine maintenance activities.""" - - def __init__(self, maintenance_cost_rate, construction_cost, discount_rate, period, design_life): - pwf = sum(1 / ((1 + discount_rate) ** (i * period)) for i in range(1, int(design_life / period) + 1)) - cost = maintenance_cost_rate * construction_cost * pwf - super().__init__(amount=cost, category="Economic", is_initial=False, is_recurring=True, present_worth_factor=pwf) - self.maintenance_cost_rate = maintenance_cost_rate - self.construction_cost = construction_cost - self.discount_rate = discount_rate - self.period = period - self.design_life = design_life - - def calculate_cost(self): - return self.maintenance_cost_rate * self.construction_cost * self.present_worth_factor - - -class PeriodicMaintenanceCarbonCost(CostComponent): - """Calculates emissions from maintenance activities.""" - - def __init__(self, material_quantity, carbon_emission_factor, carbon_cost, discount_rate, period, design_life): - pwf = sum(1 / ((1 + discount_rate) ** (i * period)) for i in range(1, int(design_life / period) + 1)) - cost = material_quantity * carbon_emission_factor * carbon_cost * pwf - super().__init__(amount=cost, category="Environmental", is_initial=False, is_recurring=True, present_worth_factor=pwf) - self.material_quantity = material_quantity - self.carbon_emission_factor = carbon_emission_factor - self.carbon_cost = carbon_cost - - def calculate_cost(self): - return self.material_quantity * self.carbon_emission_factor * self.carbon_cost * self.present_worth_factor - - -class RoutineInspectionCost(CostComponent): - """Annual cost of inspections for structural integrity.""" - - def __init__(self, quantity, rate, discount_rate, design_life): - pwf = sum(1 / ((1 + discount_rate) ** i) for i in range(1, design_life + 1)) - cost = quantity * rate * pwf - super().__init__(amount=cost, category="Economic", is_initial=False, is_recurring=True, present_worth_factor=pwf) - self.quantity = quantity - self.rate = rate - - def calculate_cost(self): - return self.quantity * self.rate * self.present_worth_factor - - -class RepairAndRehabilitationCost(CostComponent): - """Covers major structural repairs and retrofitting.""" - - def __init__(self, repair_cost_rate, construction_cost, discount_rate, period, design_life): - pwf = sum(1 / ((1 + discount_rate) ** (i * period)) for i in range(1, int(design_life / period) + 1)) - cost = repair_cost_rate * construction_cost * pwf - super().__init__(amount=cost, category="Economic", is_initial=False, is_recurring=True, present_worth_factor=pwf) - - def calculate_cost(self): - return self.amount - - -class DemolitionCost(CostComponent): - """Costs incurred at the end of bridge life for demolition and disposal.""" - - def __init__(self, demolition_rate, construction_cost, discount_rate, design_life): - pwf = 1 / ((1 + discount_rate) ** design_life) - cost = demolition_rate * construction_cost * pwf - super().__init__(amount=cost, category="Economic", is_initial=False, is_recurring=False, present_worth_factor=pwf) - - def calculate_cost(self): - return self.amount - - -class RecyclingCost(CostComponent): - """Accounts for material salvage and repurposing costs.""" - - def __init__(self, scrap_value, quantity, discount_rate, design_life): - pwf = 1 / ((1 + discount_rate) ** design_life) - cost = scrap_value * quantity * pwf - super().__init__(amount=cost, category="Economic", is_initial=False, is_recurring=False, present_worth_factor=pwf) - - def calculate_cost(self): - return self.amount diff --git a/src/osbridgelcca/core/cost_components.py b/src/osbridgelcca/core/cost_components.py new file mode 100644 index 0000000..eb00fc6 --- /dev/null +++ b/src/osbridgelcca/core/cost_components.py @@ -0,0 +1,214 @@ +from abc import ABC, abstractmethod +import pandas as pd + + + +class CostComponent(ABC): + """Abstract Base Class for different cost components in Life Cycle Cost Analysis.""" + + def __init__(self): + self.amount = 0 # To be calculated in subclasses + self.category = None # Defined in subclass + self.is_initial = None # Defined in subclass + self.is_recurring = None # Defined in subclass + self.pwf = None # To be calculated in subclasses + + @abstractmethod + def calculate_cost(self): + """Abstract method to be implemented by subclasses for cost calculation.""" + pass + + def __str__(self): + return f"{self.__class__.__name__}: Amount = {self.amount:.2f}, Category = {self.category}" + + +def calculate_pwf(discount_rate=None, design_life=None, is_initial=False, is_recurring=True, interval=1, + future_year=None): + """ + Calculates the Present Worth Factor (PWF) based on cost type. + + :param discount_rate: Discount rate (decimal form, e.g., 0.05 for 5%). + :param design_life: Total design life in years. + :param is_initial: True for initial cost (PWF = 1). + :param is_recurring: True for recurring costs. + :param interval: Interval in years for recurring costs (default is 1). + :param future_year: Year when a non-recurring future cost occurs. + :return: Present Worth Factor (PWF). + """ + if is_initial: + return 1.0 # Initial cost has no discounting + elif not is_recurring: + if future_year is None: + raise ValueError("future_year must be provided for non-recurring future costs") + return 1 / ((1 + discount_rate) ** future_year) + else: + return sum(1 / ((1 + discount_rate) ** (i * interval)) for i in range(1, int(design_life / interval) + 1)) + + +class InitialConstructionCost(CostComponent): + def __init__(self, material_boq: pd.DataFrame): + super().__init__() + self.material_boq = material_boq + self.category = "Economic" + self.is_initial = True + self.is_recurring = False + self.pwf = calculate_pwf(is_initial=True) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return (self.material_boq.iloc[:, 2] * self.material_boq.iloc[:, 3]).sum() + + +class InitialCarbonEmissionCost(CostComponent): + def __init__(self, material_boq: pd.DataFrame, carbon_cost): + super().__init__() + self.material_boq = material_boq + self.carbon_cost = carbon_cost + self.category = "Environmental" + self.is_initial = True + self.is_recurring = False + self.pwf = calculate_pwf(is_initial=True) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.carbon_cost * (self.material_boq.iloc[:, 2] * self.material_boq.iloc[:, 4]).sum() + + +class TimeCost(CostComponent): + def __init__(self, construction_cost, interest_rate, construction_time, investment_ratio): + super().__init__() + self.construction_cost = construction_cost + self.interest_rate = interest_rate + self.construction_time = construction_time + self.investment_ratio = investment_ratio + self.category = "Economic" + self.is_initial = True + self.is_recurring = False + self.pwf = calculate_pwf(is_initial=True) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.construction_cost * self.interest_rate * self.construction_time * self.investment_ratio + + +class RoadUserCost(CostComponent): + def __init__(self, vehicles_affected, vehicle_operation_cost, construction_time): + super().__init__() + self.vehicles_affected = vehicles_affected + self.vehicle_operation_cost = vehicle_operation_cost + self.construction_time = construction_time + self.delay_cost = 0.00 + self.category = "Economic" + self.is_initial = True + self.is_recurring = False + self.pwf = 1.0 + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.vehicles_affected * (self.vehicle_operation_cost + self.delay_cost) * self.construction_time + + +class ReroutingCarbonEmissionCost(CostComponent): + def __init__(self, vehicles_affected, reroute_distance, co2_emission_per_km, carbon_cost): + super().__init__() + self.vehicles_affected = vehicles_affected + self.reroute_distance = reroute_distance + self.co2_emission_per_km = co2_emission_per_km + self.carbon_cost = carbon_cost + self.category = "Environmental" + self.is_initial = True + self.is_recurring = False + self.pwf = 1.0 + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.vehicles_affected * self.reroute_distance * self.co2_emission_per_km * self.carbon_cost + + +class PeriodicMaintenanceCost(CostComponent): + def __init__(self, maintenance_cost_rate, construction_cost, discount_rate, interval, design_life): + super().__init__() + self.maintenance_cost_rate = maintenance_cost_rate + self.construction_cost = construction_cost + self.discount_rate = discount_rate + self.interval = interval + self.design_life = design_life + self.category = "Economic" + self.is_initial = False + self.is_recurring = True + self.pwf = calculate_pwf(discount_rate, design_life=self.design_life, is_recurring=self.is_recurring, + interval=interval) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.maintenance_cost_rate * self.construction_cost * self.pwf + + +class PeriodicMaintenanceCarbonEmissionCost(CostComponent): + def __init__(self, material_quantity, carbon_emission_factor, carbon_cost, discount_rate, interval, design_life): + super().__init__() + self.material_quantity = material_quantity + self.carbon_emission_factor = carbon_emission_factor + self.carbon_cost = carbon_cost + self.discount_rate = discount_rate + self.interval = interval + self.design_life = design_life + self.category = "Environmental" + self.is_initial = False + self.is_recurring = True + self.pwf = calculate_pwf(discount_rate, design_life, is_recurring=True, interval=interval) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.material_quantity * self.carbon_emission_factor * self.carbon_cost * self.pwf + + +class RoutineInspectionCost(CostComponent): + def __init__(self, quantity, rate, discount_rate, design_life): + super().__init__() + self.quantity = quantity + self.rate = rate + self.discount_rate = discount_rate + self.design_life = design_life + self.category = "Economic" + self.is_initial = False + self.is_recurring = True + self.pwf = calculate_pwf(discount_rate, design_life, is_recurring=True) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.quantity * self.rate * self.pwf + + +class DemolitionCost(CostComponent): + def __init__(self, demolition_rate, construction_cost, discount_rate, design_life): + super().__init__() + self.demolition_rate = demolition_rate + self.construction_cost = construction_cost + self.discount_rate = discount_rate + self.design_life = design_life + self.category = "Economic" + self.is_initial = False + self.is_recurring = False + self.pwf = calculate_pwf(discount_rate, design_life, is_recurring=False, future_year=design_life) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.demolition_rate * self.construction_cost * self.pwf + + +class RecyclingCost(CostComponent): + def __init__(self, scrap_value, quantity, discount_rate, design_life): + super().__init__() + self.scrap_value = scrap_value + self.quantity = quantity + self.discount_rate = discount_rate + self.design_life = design_life + self.category = "Economic" + self.is_initial = False + self.is_recurring = False + self.pwf = calculate_pwf(discount_rate, design_life, is_recurring=False, future_year=design_life) + self.amount = self.calculate_cost() + + def calculate_cost(self): + return self.scrap_value * self.quantity * self.pwf diff --git a/src/osbridgelcca/web_app/src/components/InputForm.jsx b/src/osbridgelcca/web_app/src_web/components/InputForm.jsx similarity index 100% rename from src/osbridgelcca/web_app/src/components/InputForm.jsx rename to src/osbridgelcca/web_app/src_web/components/InputForm.jsx diff --git a/src/osbridgelcca/web_app/src/components/ResultsView.jsx b/src/osbridgelcca/web_app/src_web/components/ResultsView.jsx similarity index 100% rename from src/osbridgelcca/web_app/src/components/ResultsView.jsx rename to src/osbridgelcca/web_app/src_web/components/ResultsView.jsx diff --git a/src/osbridgelcca/web_app/src/services/api.js b/src/osbridgelcca/web_app/src_web/services/api.js similarity index 100% rename from src/osbridgelcca/web_app/src/services/api.js rename to src/osbridgelcca/web_app/src_web/services/api.js diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a0993b8..afb922a 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -1,12 +1,5 @@ -import pytest -from backend.main import app as flask_app +import sys +import os -@pytest.fixture -def client(): - """Flask test client""" - return flask_app.test_client() - -@pytest.fixture -def sample_cost_data(): - """Sample cost data for testing""" - return {"quantity": 100, "rate": 500} +# Add the src_web directory to sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../src_web'))) diff --git a/tests/unit_tests/test_cost_components.py b/tests/unit_tests/test_cost_components.py new file mode 100644 index 0000000..9f0fc0b --- /dev/null +++ b/tests/unit_tests/test_cost_components.py @@ -0,0 +1,58 @@ +import pytest +from src.osbridgelcca.core.cost_components import ( + InitialConstructionCost, + InitialCarbonEmissionCost, + TimeCost, + RoadUserCost, + ReroutingCarbonEmissionCost, + PeriodicMaintenanceCost, + PeriodicMaintenanceCarbonCost, + RoutineInspectionCost, + RepairAndRehabilitationCost, + DemolitionCost, + RecyclingCost, +) + +def test_initial_construction_cost(): + cost = InitialConstructionCost(quantity=100, rate=50) + assert cost.calculate_cost() == 5000 + +def test_initial_carbon_emission_cost(): + cost = InitialCarbonEmissionCost(material_quantity=100, carbon_emission_factor=2, carbon_cost=10) + assert cost.calculate_cost() == 2000 + +def test_time_cost(): + cost = TimeCost(construction_cost=10000, interest_rate=0.05, construction_time=2, investment_ratio=0.8) + assert cost.calculate_cost() == 800.0 + +def test_road_user_cost(): + cost = RoadUserCost(vehicles_affected=1000, vehicle_operation_cost=2, construction_time=5) + assert cost.calculate_cost() == 10000 + +def test_additional_carbon_emission_cost(): + cost = ReroutingCarbonEmissionCost(vehicles_affected=1000, reroute_distance=10, co2_emission_per_km=0.5, carbon_cost=20) + assert cost.calculate_cost() == 100000.0 + +def test_periodic_maintenance_cost(): + cost = PeriodicMaintenanceCost(maintenance_cost_rate=0.02, construction_cost=100000, discount_rate=0.03, period=5, design_life=50) + assert cost.calculate_cost() > 0 # Ensure cost is calculated + +def test_periodic_maintenance_carbon_cost(): + cost = PeriodicMaintenanceCarbonCost(material_quantity=500, carbon_emission_factor=1.5, carbon_cost=20, discount_rate=0.03, period=5, design_life=50) + assert cost.calculate_cost() > 0 + +def test_routine_inspection_cost(): + cost = RoutineInspectionCost(quantity=10, rate=1000, discount_rate=0.03, design_life=50) + assert cost.calculate_cost() > 0 + +def test_repair_and_rehabilitation_cost(): + cost = RepairAndRehabilitationCost(repair_cost_rate=0.1, construction_cost=100000, discount_rate=0.03, period=10, design_life=50) + assert cost.calculate_cost() > 0 + +def test_demolition_cost(): + cost = DemolitionCost(demolition_rate=0.05, construction_cost=100000, discount_rate=0.03, design_life=50) + assert cost.calculate_cost() > 0 + +def test_recycling_cost(): + cost = RecyclingCost(scrap_value=1000, quantity=10, discount_rate=0.03, design_life=50) + assert cost.calculate_cost() > 0