feat: enhance database models with metadata and new resource types
This commit is contained in:
@@ -1,6 +1,16 @@
|
|||||||
"""Database models for the CalMiner domain."""
|
"""Database models and shared metadata for the CalMiner domain."""
|
||||||
|
|
||||||
from .financial_input import FinancialCategory, FinancialInput
|
from .financial_input import FinancialCategory, FinancialInput
|
||||||
|
from .metadata import (
|
||||||
|
COST_BUCKET_METADATA,
|
||||||
|
RESOURCE_METADATA,
|
||||||
|
STOCHASTIC_VARIABLE_METADATA,
|
||||||
|
CostBucket,
|
||||||
|
ResourceDescriptor,
|
||||||
|
ResourceType,
|
||||||
|
StochasticVariable,
|
||||||
|
StochasticVariableDescriptor,
|
||||||
|
)
|
||||||
from .project import MiningOperationType, Project
|
from .project import MiningOperationType, Project
|
||||||
from .scenario import Scenario, ScenarioStatus
|
from .scenario import Scenario, ScenarioStatus
|
||||||
from .simulation_parameter import DistributionType, SimulationParameter
|
from .simulation_parameter import DistributionType, SimulationParameter
|
||||||
@@ -14,4 +24,12 @@ __all__ = [
|
|||||||
"ScenarioStatus",
|
"ScenarioStatus",
|
||||||
"DistributionType",
|
"DistributionType",
|
||||||
"SimulationParameter",
|
"SimulationParameter",
|
||||||
|
"ResourceType",
|
||||||
|
"CostBucket",
|
||||||
|
"StochasticVariable",
|
||||||
|
"RESOURCE_METADATA",
|
||||||
|
"COST_BUCKET_METADATA",
|
||||||
|
"STOCHASTIC_VARIABLE_METADATA",
|
||||||
|
"ResourceDescriptor",
|
||||||
|
"StochasticVariableDescriptor",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ from datetime import date, datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Date,
|
Date,
|
||||||
DateTime,
|
DateTime,
|
||||||
@@ -18,6 +30,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from config.database import Base
|
from config.database import Base
|
||||||
|
from .metadata import CostBucket
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from .scenario import Scenario
|
from .scenario import Scenario
|
||||||
@@ -46,6 +59,9 @@ class FinancialInput(Base):
|
|||||||
category: Mapped[FinancialCategory] = mapped_column(
|
category: Mapped[FinancialCategory] = mapped_column(
|
||||||
SQLEnum(FinancialCategory), nullable=False
|
SQLEnum(FinancialCategory), nullable=False
|
||||||
)
|
)
|
||||||
|
cost_bucket: Mapped[CostBucket | None] = mapped_column(
|
||||||
|
SQLEnum(CostBucket), nullable=True
|
||||||
|
)
|
||||||
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||||
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
||||||
effective_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
effective_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
@@ -59,5 +75,14 @@ class FinancialInput(Base):
|
|||||||
|
|
||||||
scenario: Mapped["Scenario"] = relationship("Scenario", back_populates="financial_inputs")
|
scenario: Mapped["Scenario"] = relationship("Scenario", back_populates="financial_inputs")
|
||||||
|
|
||||||
|
@validates("currency")
|
||||||
|
def _validate_currency(self, key: str, value: str | None) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
value = value.upper()
|
||||||
|
if len(value) != 3:
|
||||||
|
raise ValueError("Currency code must be a 3-letter ISO 4217 value")
|
||||||
|
return value
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma: no cover
|
def __repr__(self) -> str: # pragma: no cover
|
||||||
return f"FinancialInput(id={self.id!r}, scenario_id={self.scenario_id!r}, name={self.name!r})"
|
return f"FinancialInput(id={self.id!r}, scenario_id={self.scenario_id!r}, name={self.name!r})"
|
||||||
|
|||||||
146
models/metadata.py
Normal file
146
models/metadata.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceType(str, Enum):
|
||||||
|
"""Primary consumables and resources used in mining operations."""
|
||||||
|
|
||||||
|
DIESEL = "diesel"
|
||||||
|
ELECTRICITY = "electricity"
|
||||||
|
WATER = "water"
|
||||||
|
EXPLOSIVES = "explosives"
|
||||||
|
REAGENTS = "reagents"
|
||||||
|
LABOR = "labor"
|
||||||
|
EQUIPMENT_HOURS = "equipment_hours"
|
||||||
|
TAILINGS_CAPACITY = "tailings_capacity"
|
||||||
|
|
||||||
|
|
||||||
|
class CostBucket(str, Enum):
|
||||||
|
"""Granular cost buckets aligned with project accounting."""
|
||||||
|
|
||||||
|
CAPITAL_INITIAL = "capital_initial"
|
||||||
|
CAPITAL_SUSTAINING = "capital_sustaining"
|
||||||
|
OPERATING_FIXED = "operating_fixed"
|
||||||
|
OPERATING_VARIABLE = "operating_variable"
|
||||||
|
MAINTENANCE = "maintenance"
|
||||||
|
RECLAMATION = "reclamation"
|
||||||
|
ROYALTIES = "royalties"
|
||||||
|
GENERAL_ADMIN = "general_admin"
|
||||||
|
|
||||||
|
|
||||||
|
class StochasticVariable(str, Enum):
|
||||||
|
"""Domain variables that typically require probabilistic modelling."""
|
||||||
|
|
||||||
|
ORE_GRADE = "ore_grade"
|
||||||
|
RECOVERY_RATE = "recovery_rate"
|
||||||
|
METAL_PRICE = "metal_price"
|
||||||
|
OPERATING_COST = "operating_cost"
|
||||||
|
CAPITAL_COST = "capital_cost"
|
||||||
|
DISCOUNT_RATE = "discount_rate"
|
||||||
|
THROUGHPUT = "throughput"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResourceDescriptor:
|
||||||
|
"""Describes canonical metadata for a resource type."""
|
||||||
|
|
||||||
|
unit: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
RESOURCE_METADATA: dict[ResourceType, ResourceDescriptor] = {
|
||||||
|
ResourceType.DIESEL: ResourceDescriptor(unit="L", description="Diesel fuel consumption"),
|
||||||
|
ResourceType.ELECTRICITY: ResourceDescriptor(unit="kWh", description="Electrical power usage"),
|
||||||
|
ResourceType.WATER: ResourceDescriptor(unit="m3", description="Process and dust suppression water"),
|
||||||
|
ResourceType.EXPLOSIVES: ResourceDescriptor(unit="kg", description="Blasting agent consumption"),
|
||||||
|
ResourceType.REAGENTS: ResourceDescriptor(unit="kg", description="Processing reagents"),
|
||||||
|
ResourceType.LABOR: ResourceDescriptor(unit="hours", description="Direct labor hours"),
|
||||||
|
ResourceType.EQUIPMENT_HOURS: ResourceDescriptor(unit="hours", description="Mobile equipment operating hours"),
|
||||||
|
ResourceType.TAILINGS_CAPACITY: ResourceDescriptor(unit="m3", description="Tailings storage usage"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CostBucketDescriptor:
|
||||||
|
"""Describes reporting label and guidance for a cost bucket."""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
COST_BUCKET_METADATA: dict[CostBucket, CostBucketDescriptor] = {
|
||||||
|
CostBucket.CAPITAL_INITIAL: CostBucketDescriptor(
|
||||||
|
label="Initial Capital",
|
||||||
|
description="Pre-production capital required to construct the mine",
|
||||||
|
),
|
||||||
|
CostBucket.CAPITAL_SUSTAINING: CostBucketDescriptor(
|
||||||
|
label="Sustaining Capital",
|
||||||
|
description="Ongoing capital investments to maintain operations",
|
||||||
|
),
|
||||||
|
CostBucket.OPERATING_FIXED: CostBucketDescriptor(
|
||||||
|
label="Fixed Operating",
|
||||||
|
description="Fixed operating costs independent of production rate",
|
||||||
|
),
|
||||||
|
CostBucket.OPERATING_VARIABLE: CostBucketDescriptor(
|
||||||
|
label="Variable Operating",
|
||||||
|
description="Costs that scale with throughput or production",
|
||||||
|
),
|
||||||
|
CostBucket.MAINTENANCE: CostBucketDescriptor(
|
||||||
|
label="Maintenance",
|
||||||
|
description="Maintenance and repair expenditures",
|
||||||
|
),
|
||||||
|
CostBucket.RECLAMATION: CostBucketDescriptor(
|
||||||
|
label="Reclamation",
|
||||||
|
description="Mine closure and reclamation liabilities",
|
||||||
|
),
|
||||||
|
CostBucket.ROYALTIES: CostBucketDescriptor(
|
||||||
|
label="Royalties",
|
||||||
|
description="Royalty and streaming obligations",
|
||||||
|
),
|
||||||
|
CostBucket.GENERAL_ADMIN: CostBucketDescriptor(
|
||||||
|
label="G&A",
|
||||||
|
description="Corporate and site general and administrative costs",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StochasticVariableDescriptor:
|
||||||
|
"""Metadata describing how a stochastic variable is typically modelled."""
|
||||||
|
|
||||||
|
unit: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
STOCHASTIC_VARIABLE_METADATA: dict[StochasticVariable, StochasticVariableDescriptor] = {
|
||||||
|
StochasticVariable.ORE_GRADE: StochasticVariableDescriptor(
|
||||||
|
unit="g/t",
|
||||||
|
description="Head grade variability across the ore body",
|
||||||
|
),
|
||||||
|
StochasticVariable.RECOVERY_RATE: StochasticVariableDescriptor(
|
||||||
|
unit="%",
|
||||||
|
description="Metallurgical recovery uncertainty",
|
||||||
|
),
|
||||||
|
StochasticVariable.METAL_PRICE: StochasticVariableDescriptor(
|
||||||
|
unit="$/unit",
|
||||||
|
description="Commodity price fluctuations",
|
||||||
|
),
|
||||||
|
StochasticVariable.OPERATING_COST: StochasticVariableDescriptor(
|
||||||
|
unit="$/t",
|
||||||
|
description="Operating cost per tonne volatility",
|
||||||
|
),
|
||||||
|
StochasticVariable.CAPITAL_COST: StochasticVariableDescriptor(
|
||||||
|
unit="$",
|
||||||
|
description="Capital cost overrun/underrun potential",
|
||||||
|
),
|
||||||
|
StochasticVariable.DISCOUNT_RATE: StochasticVariableDescriptor(
|
||||||
|
unit="%",
|
||||||
|
description="Discount rate sensitivity",
|
||||||
|
),
|
||||||
|
StochasticVariable.THROUGHPUT: StochasticVariableDescriptor(
|
||||||
|
unit="t/d",
|
||||||
|
description="Plant throughput variability",
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -19,8 +19,10 @@ class MiningOperationType(str, Enum):
|
|||||||
|
|
||||||
OPEN_PIT = "open_pit"
|
OPEN_PIT = "open_pit"
|
||||||
UNDERGROUND = "underground"
|
UNDERGROUND = "underground"
|
||||||
|
IN_SITU_LEACH = "in_situ_leach"
|
||||||
PLACER = "placer"
|
PLACER = "placer"
|
||||||
QUARRY = "quarry"
|
QUARRY = "quarry"
|
||||||
|
MOUNTAINTOP_REMOVAL = "mountaintop_removal"
|
||||||
OTHER = "other"
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ from datetime import date, datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from sqlalchemy import Date, DateTime, Enum as SQLEnum, ForeignKey, Integer, Numeric, String, Text
|
from sqlalchemy import (
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from config.database import Base
|
from config.database import Base
|
||||||
|
from .metadata import ResourceType
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from .financial_input import FinancialInput
|
from .financial_input import FinancialInput
|
||||||
@@ -42,6 +52,9 @@ class Scenario(Base):
|
|||||||
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
discount_rate: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
|
discount_rate: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
|
||||||
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
currency: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
||||||
|
primary_resource: Mapped[ResourceType | None] = mapped_column(
|
||||||
|
SQLEnum(ResourceType), nullable=True
|
||||||
|
)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from config.database import Base
|
from config.database import Base
|
||||||
|
from .metadata import ResourceType, StochasticVariable
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from .scenario import Scenario
|
from .scenario import Scenario
|
||||||
@@ -45,6 +46,12 @@ class SimulationParameter(Base):
|
|||||||
distribution: Mapped[DistributionType] = mapped_column(
|
distribution: Mapped[DistributionType] = mapped_column(
|
||||||
SQLEnum(DistributionType), nullable=False
|
SQLEnum(DistributionType), nullable=False
|
||||||
)
|
)
|
||||||
|
variable: Mapped[StochasticVariable | None] = mapped_column(
|
||||||
|
SQLEnum(StochasticVariable), nullable=True
|
||||||
|
)
|
||||||
|
resource_type: Mapped[ResourceType | None] = mapped_column(
|
||||||
|
SQLEnum(ResourceType), nullable=True
|
||||||
|
)
|
||||||
mean_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
mean_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
standard_deviation: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
standard_deviation: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
minimum_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
minimum_value: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user