diff --git a/.gitignore b/.gitignore
index ebc2e6c..cb4f549 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,4 @@ logs/
# SQLite database
*.sqlite3
+test.db
diff --git a/README.md b/README.md
index e77a798..1218f85 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,15 @@ A range of features are implemented to support these functionalities.
## Features
-- **Scenario Management**: The database supports different scenarios for what-if analysis, with parent-child relationships between scenarios.
-- **Monte Carlo Simulation**: The system can perform Monte Carlo simulations for risk analysis and probabilistic forecasting.
-- **Stochastic Variables**: The model handles uncertainty by defining variables with probability distributions.
-- **Cost Tracking**: It tracks capital (`capex`) and operational (`opex`) expenditures.
-- **Consumption Tracking**: It monitors the consumption of resources like chemicals, fuel, water, and scrap materials.
-- **Production Output**: The database stores production results, including tons produced, recovery rates, and revenue.
-- **Process Parameters**: It allows for defining and storing various parameters for different processes and scenarios.
-- **Equipment Management**: The system manages equipment and their operational data.
-- **Maintenance Logging**: It includes a log for equipment maintenance events.
+- **Scenario Management**: Manage multiple mining scenarios with independent parameter sets and outputs.
+- **Process Parameters**: Define and persist process inputs via FastAPI endpoints and template-driven forms.
+- **Cost Tracking**: Capture capital (`capex`) and operational (`opex`) expenditures per scenario.
+- **Consumption Tracking**: Record resource consumption (chemicals, fuel, water, scrap) tied to scenarios.
+- **Production Output**: Store production metrics such as tonnage, recovery, and revenue drivers.
+- **Equipment Management**: Register scenario-specific equipment inventories.
+- **Maintenance Logging**: Log maintenance events against equipment with dates and costs.
+- **Reporting Dashboard**: Surface aggregated statistics for simulation outputs with an interactive Chart.js dashboard.
+- **Monte Carlo Simulation (in progress)**: Services and routes are scaffolded for future stochastic analysis.
## Architecture
@@ -66,10 +66,36 @@ pip install -r requirements.txt
uvicorn main:app --reload
```
+## Usage Overview
+
+- **API base URL**: `http://localhost:8000/api`
+- **Key routes**:
+ - `POST /api/scenarios/` create scenarios
+ - `POST /api/parameters/` manage process parameters
+ - `POST /api/costs/capex` and `POST /api/costs/opex` capture project costs
+ - `POST /api/consumption/` add consumption entries
+ - `POST /api/production/` register production output
+ - `POST /api/equipment/` create equipment records
+ - `POST /api/maintenance/` log maintenance events
+ - `POST /api/reporting/summary` aggregate simulation results
+
+### Dashboard Preview
+
+1. Start the FastAPI server and navigate to `/dashboard` (served by `templates/Dashboard.html`).
+2. Use the "Load Sample Data" button to populate the JSON textarea with demo results.
+3. Select "Refresh Dashboard" to view calculated statistics and a distribution chart sourced from `/api/reporting/summary`.
+4. Paste your own simulation outputs (array of objects containing a numeric `result` property) to visualize custom runs.
+
## Testing
Testing guidelines and best practices are outlined in [docs/testing.md](docs/testing.md).
+To execute the unit test suite:
+
+```powershell
+pytest
+```
+
## Database Objects
The database is composed of several tables that store different types of information.
diff --git a/components/Dashboard.html b/components/Dashboard.html
deleted file mode 100644
index d4f06bf..0000000
--- a/components/Dashboard.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- CalMiner Dashboard
-
-
-
Simulation Results Dashboard
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/architecture.md b/docs/architecture.md
index 0d1c459..4928935 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -2,40 +2,47 @@
## Overview
-CalMiner is a web application for planning mining projects, estimating costs, returns, and profitability. It uses Monte Carlo simulations for risk analysis and supports multiple scenarios.
+CalMiner is a FastAPI application that collects mining project inputs, persists scenario-specific records, and surfaces aggregated insights. The platform targets Monte Carlo driven planning, with deterministic CRUD features in place and simulation logic staged for future work.
## System Components
-- **Frontend**: Web interface for user interaction (to be defined).
-- **Backend**: Python API server (e.g., FastAPI) handling business logic.
-- **Database**: PostgreSQL.
-- **Configuration**: Environment variables and settings loaded via `python-dotenv` and stored in `config/` directory.
-- **Simulation Engine**: Python-based Monte Carlo runs and stochastic calculations.
-- **API Routes**: FastAPI routers defined in `routes/` for scenarios, simulations, consumptions, and reporting endpoints.
+- **FastAPI backend** (`main.py`, `routes/`): hosts REST endpoints for scenarios, parameters, costs, consumption, production, equipment, maintenance, simulations, and reporting. Each router encapsulates request/response schemas and DB access patterns.
+- **Service layer** (`services/`): houses business logic. `services/reporting.py` produces statistical summaries, while `services/simulation.py` provides the Monte Carlo integration point.
+- **Persistence** (`models/`, `config/database.py`): SQLAlchemy models map to PostgreSQL tables in schema `bricsium_platform`. Relationships connect scenarios to derived domain entities.
+- **Presentation** (`templates/`, `components/`): server-rendered views support data entry (scenario and parameter forms) and the dashboard visualization powered by Chart.js.
+- **Middleware** (`middleware/validation.py`): applies JSON validation before requests reach routers.
+- **Testing** (`tests/unit/`): pytest suite covering route and service behavior.
-## Data Flow
+## Runtime Flow
-1. User inputs scenario parameters via frontend.
-2. Backend validates and stores in database.
-3. Simulation engine runs Monte Carlo iterations using stochastic variables.
-4. Results stored in `simulation_result` table.
-5. Frontend displays outputs like NPV, IRR, EBITDA.
+1. Users navigate to form templates or API clients to manage scenarios, parameters, and operational data.
+2. FastAPI routers validate payloads with Pydantic models, then delegate to SQLAlchemy sessions for persistence.
+3. Simulation runs (placeholder `services/simulation.py`) will consume stored parameters to emit iteration results via `/api/simulations/run`.
+4. Reporting requests POST simulation outputs to `/api/reporting/summary`; the reporting service calculates aggregates (count, min/max, mean, median, percentiles, standard deviation).
+5. `templates/Dashboard.html` fetches summaries, renders metric cards, and plots distribution charts with Chart.js for stakeholder review.
-## Database Architecture
+## Data Model Highlights
-- Schema: `bricsium_platform`
-- Key tables include:
+- `scenario`: central entity describing a mining scenario; owns relationships to cost, consumption, production, equipment, and maintenance tables.
+- `capex`, `opex`: monetary tracking linked to scenarios.
+- `consumption`: resource usage entries parameterized by scenario and description.
+- `production_output`: production metrics per scenario.
+- `equipment` and `maintenance`: equipment inventory and maintenance events with dates/costs.
+- `simulation_result`: staging table for future Monte Carlo outputs (not yet populated by `run_simulation`).
- - `scenario` (scenario metadata and parameters)
- - `capex`, `opex` (capital and operational expenditures)
- - `chemical_consumption`, `fuel_consumption`, `water_consumption`, `scrap_consumption`
- - `production_output`, `equipment_operation`, `ore_batch`
- - `exchange_rate`, `simulation_result`
+Foreign keys secure referential integrity between domain tables and their scenarios, enabling per-scenario analytics.
-- Relationships: Foreign keys link scenarios to parameters, consumptions, and simulation results.
+## Integrations and Future Work
-## Next Steps
+- **Monte Carlo engine**: `services/simulation.py` will incorporate stochastic sampling (e.g., NumPy, SciPy) to populate `simulation_result` and feed reporting.
+- **Persistence of results**: `/api/simulations/run` currently returns in-memory results; next iteration should persist to `simulation_result` and reference scenarios.
+- **Authentication**: not yet implemented; all endpoints are open.
+- **Deployment**: documentation focuses on local development; containerization and CI/CD pipelines remain to be defined.
-- Define API endpoints.
-- Implement simulation logic.
-- Add authentication and user management.
+For extended diagrams and setup instructions reference:
+
+- [docs/development_setup.md](development_setup.md) — environment provisioning and tooling.
+- [docs/testing.md](testing.md) — pytest workflow and coverage expectations.
+- [docs/mvp.md](mvp.md) — roadmap and milestone scope.
+- [docs/implementation_plan.md](implementation_plan.md) — feature breakdown aligned with the TODO tracker.
+- [docs/architecture_overview.md](architecture_overview.md) — supplementary module map and request flow diagram.
diff --git a/docs/architecture_overview.md b/docs/architecture_overview.md
new file mode 100644
index 0000000..30dea3f
--- /dev/null
+++ b/docs/architecture_overview.md
@@ -0,0 +1,44 @@
+# Architecture Overview
+
+This overview complements `docs/architecture.md` with a high-level map of CalMiner's module layout and request flow.
+
+## Module Map
+
+- `main.py`: FastAPI entry point bootstrapping routers and middleware.
+- `models/`: SQLAlchemy declarative models for all database tables. Key modules:
+ - `scenario.py`: central scenario entity with relationships to cost, consumption, production, equipment, maintenance, and simulation results.
+ - `capex.py`, `opex.py`: financial expenditures tied to scenarios.
+ - `consumption.py`, `production_output.py`: operational data tables.
+ - `equipment.py`, `maintenance.py`: asset management models.
+- `routes/`: REST endpoints grouped by domain (scenarios, parameters, costs, consumption, production, equipment, maintenance, reporting, simulations, UI).
+- `services/`: business logic abstractions. `reporting.py` supplies summary statistics; `simulation.py` hosts the Monte Carlo extension point.
+- `middleware/validation.py`: request JSON validation prior to hitting routers.
+- `templates/`: Jinja2 templates for UI (scenario form, parameter input, dashboard).
+
+## Request Flow
+
+```mermaid
+graph TD
+ A[Browser / API Client] -->|HTTP| B[FastAPI Router]
+ B --> C[Dependency Injection]
+ C --> D[SQLAlchemy Session]
+ B --> E[Service Layer]
+ E --> D
+ E --> F[Reporting / Simulation Logic]
+ D --> G[PostgreSQL]
+ F --> H[Summary Response]
+ G --> H
+ H --> A
+```
+
+## Dashboard Interaction
+
+1. User loads `/dashboard`, served by `templates/Dashboard.html`.
+2. Template fetches `/api/reporting/summary` with sample or user-provided simulation outputs.
+3. Response metrics populate the summary grid and Chart.js visualization.
+
+## Simulation Roadmap
+
+- Implement stochastic sampling in `services/simulation.py` (e.g., NumPy random draws based on parameter distributions).
+- Store iterations in `models/simulation_result.py` via `/api/simulations/run`.
+- Feed persisted results into reporting for downstream analytics and historical comparisons.
diff --git a/models/maintenance.py b/models/maintenance.py
index d5f4672..43a7aea 100644
--- a/models/maintenance.py
+++ b/models/maintenance.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func
+from sqlalchemy import Column, Date, Float, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from config.database import Base
@@ -7,11 +7,17 @@ class Maintenance(Base):
__tablename__ = "maintenance"
id = Column(Integer, primary_key=True, index=True)
+ equipment_id = Column(Integer, ForeignKey("equipment.id"), nullable=False)
scenario_id = Column(Integer, ForeignKey("scenario.id"), nullable=False)
- performed_at = Column(DateTime(timezone=True), server_default=func.now())
- details = Column(String, nullable=True)
+ maintenance_date = Column(Date, nullable=False)
+ description = Column(String, nullable=True)
+ cost = Column(Float, nullable=False)
+ equipment = relationship("Equipment")
scenario = relationship("Scenario", back_populates="maintenance_items")
- def __repr__(self):
- return f""
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
diff --git a/requirements.txt b/requirements.txt
index 2c4baca..f9f184d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,10 @@ uvicorn
sqlalchemy
psycopg2-binary
python-dotenv
+httpx
+jinja2
+pandas
+numpy
pytest
pytest-cov
-jinja2
\ No newline at end of file
+pytest-httpx
diff --git a/routes/consumption.py b/routes/consumption.py
index 982048c..824e6e4 100644
--- a/routes/consumption.py
+++ b/routes/consumption.py
@@ -1,10 +1,13 @@
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.orm import Session
from typing import List, Optional
-from pydantic import BaseModel
+
+from fastapi import APIRouter, Depends, status
+from pydantic import BaseModel, PositiveFloat
+from sqlalchemy.orm import Session
+
from config.database import SessionLocal
from models.consumption import Consumption
+
router = APIRouter(prefix="/api/consumption", tags=["Consumption"])
@@ -16,22 +19,25 @@ def get_db():
db.close()
-# Pydantic schemas
-class ConsumptionCreate(BaseModel):
+class ConsumptionBase(BaseModel):
scenario_id: int
- amount: float
+ amount: PositiveFloat
description: Optional[str] = None
-class ConsumptionRead(ConsumptionCreate):
+class ConsumptionCreate(ConsumptionBase):
+ pass
+
+
+class ConsumptionRead(ConsumptionBase):
id: int
class Config:
orm_mode = True
-@router.post("/", response_model=ConsumptionRead)
-async def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_db)):
+@router.post("/", response_model=ConsumptionRead, status_code=status.HTTP_201_CREATED)
+def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_db)):
db_item = Consumption(**item.dict())
db.add(db_item)
db.commit()
@@ -40,5 +46,5 @@ async def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_
@router.get("/", response_model=List[ConsumptionRead])
-async def list_consumption(db: Session = Depends(get_db)):
+def list_consumption(db: Session = Depends(get_db)):
return db.query(Consumption).all()
diff --git a/routes/maintenance.py b/routes/maintenance.py
index ea1a40f..c69480c 100644
--- a/routes/maintenance.py
+++ b/routes/maintenance.py
@@ -1,11 +1,14 @@
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.orm import Session
+from datetime import date
from typing import List, Optional
-from pydantic import BaseModel
-from datetime import datetime
+
+from fastapi import APIRouter, Depends, HTTPException, status
+from pydantic import BaseModel, PositiveFloat
+from sqlalchemy.orm import Session
+
from config.database import SessionLocal
from models.maintenance import Maintenance
+
router = APIRouter(prefix="/api/maintenance", tags=["Maintenance"])
@@ -17,29 +20,75 @@ def get_db():
db.close()
-# Pydantic schemas
-class MaintenanceCreate(BaseModel):
+class MaintenanceBase(BaseModel):
+ equipment_id: int
scenario_id: int
- details: Optional[str] = None
+ maintenance_date: date
+ description: Optional[str] = None
+ cost: PositiveFloat
-class MaintenanceRead(MaintenanceCreate):
+class MaintenanceCreate(MaintenanceBase):
+ pass
+
+
+class MaintenanceUpdate(MaintenanceBase):
+ pass
+
+
+class MaintenanceRead(MaintenanceBase):
id: int
- performed_at: datetime
class Config:
orm_mode = True
-@router.post("/", response_model=MaintenanceRead)
-async def create_maintenance(item: MaintenanceCreate, db: Session = Depends(get_db)):
- db_item = Maintenance(**item.dict())
- db.add(db_item)
+def _get_maintenance_or_404(db: Session, maintenance_id: int) -> Maintenance:
+ maintenance = db.query(Maintenance).filter(
+ Maintenance.id == maintenance_id).first()
+ if maintenance is None:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Maintenance record {maintenance_id} not found",
+ )
+ return maintenance
+
+
+@router.post("/", response_model=MaintenanceRead, status_code=status.HTTP_201_CREATED)
+def create_maintenance(maintenance: MaintenanceCreate, db: Session = Depends(get_db)):
+ db_maintenance = Maintenance(**maintenance.dict())
+ db.add(db_maintenance)
db.commit()
- db.refresh(db_item)
- return db_item
+ db.refresh(db_maintenance)
+ return db_maintenance
@router.get("/", response_model=List[MaintenanceRead])
-async def list_maintenance(db: Session = Depends(get_db)):
- return db.query(Maintenance).all()
+def list_maintenance(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
+ return db.query(Maintenance).offset(skip).limit(limit).all()
+
+
+@router.get("/{maintenance_id}", response_model=MaintenanceRead)
+def get_maintenance(maintenance_id: int, db: Session = Depends(get_db)):
+ return _get_maintenance_or_404(db, maintenance_id)
+
+
+@router.put("/{maintenance_id}", response_model=MaintenanceRead)
+def update_maintenance(
+ maintenance_id: int,
+ payload: MaintenanceUpdate,
+ db: Session = Depends(get_db),
+):
+ db_maintenance = _get_maintenance_or_404(db, maintenance_id)
+ for field, value in payload.dict().items():
+ setattr(db_maintenance, field, value)
+ db.commit()
+ db.refresh(db_maintenance)
+ return db_maintenance
+
+
+@router.delete("/{maintenance_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_maintenance(maintenance_id: int, db: Session = Depends(get_db)):
+ db_maintenance = _get_maintenance_or_404(db, maintenance_id)
+ db.delete(db_maintenance)
+ db.commit()
diff --git a/routes/production.py b/routes/production.py
index 7585ff9..8df04d5 100644
--- a/routes/production.py
+++ b/routes/production.py
@@ -1,10 +1,13 @@
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.orm import Session
from typing import List, Optional
-from pydantic import BaseModel
+
+from fastapi import APIRouter, Depends, status
+from pydantic import BaseModel, PositiveFloat
+from sqlalchemy.orm import Session
+
from config.database import SessionLocal
from models.production_output import ProductionOutput
+
router = APIRouter(prefix="/api/production", tags=["Production"])
@@ -16,22 +19,25 @@ def get_db():
db.close()
-# Pydantic schemas
-class ProductionOutputCreate(BaseModel):
+class ProductionOutputBase(BaseModel):
scenario_id: int
- amount: float
+ amount: PositiveFloat
description: Optional[str] = None
-class ProductionOutputRead(ProductionOutputCreate):
+class ProductionOutputCreate(ProductionOutputBase):
+ pass
+
+
+class ProductionOutputRead(ProductionOutputBase):
id: int
class Config:
orm_mode = True
-@router.post("/", response_model=ProductionOutputRead)
-async def create_production(item: ProductionOutputCreate, db: Session = Depends(get_db)):
+@router.post("/", response_model=ProductionOutputRead, status_code=status.HTTP_201_CREATED)
+def create_production(item: ProductionOutputCreate, db: Session = Depends(get_db)):
db_item = ProductionOutput(**item.dict())
db.add(db_item)
db.commit()
@@ -40,5 +46,5 @@ async def create_production(item: ProductionOutputCreate, db: Session = Depends(
@router.get("/", response_model=List[ProductionOutputRead])
-async def list_production(db: Session = Depends(get_db)):
+def list_production(db: Session = Depends(get_db)):
return db.query(ProductionOutput).all()
diff --git a/routes/reporting.py b/routes/reporting.py
index c4adcff..cbb2244 100644
--- a/routes/reporting.py
+++ b/routes/reporting.py
@@ -1,9 +1,11 @@
-from fastapi import APIRouter, HTTPException, Request
-from typing import Dict, Any
+from typing import Any, Dict, List
+
+from fastapi import APIRouter, HTTPException, Request, status
+from pydantic import BaseModel
-from services.reporting import generate_report
-from sqlalchemy.orm import Session
from config.database import SessionLocal
+from services.reporting import generate_report
+
router = APIRouter(prefix="/api/reporting", tags=["Reporting"])
@@ -16,11 +18,53 @@ def get_db():
db.close()
-@router.post("/summary", response_model=Dict[str, float])
+def _validate_payload(payload: Any) -> List[Dict[str, float]]:
+ if not isinstance(payload, list):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid input format",
+ )
+
+ validated: List[Dict[str, float]] = []
+ for index, item in enumerate(payload):
+ if not isinstance(item, dict):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Entry at index {index} must be an object",
+ )
+ value = item.get("result")
+ if not isinstance(value, (int, float)):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Entry at index {index} must include numeric 'result'",
+ )
+ validated.append({"result": float(value)})
+ return validated
+
+
+class ReportSummary(BaseModel):
+ count: int
+ mean: float
+ median: float
+ min: float
+ max: float
+ std_dev: float
+ percentile_10: float
+ percentile_90: float
+
+
+@router.post("/summary", response_model=ReportSummary)
async def summary_report(request: Request):
- # Read raw JSON to handle invalid input formats
- data = await request.json()
- if not isinstance(data, list):
- raise HTTPException(status_code=400, detail="Invalid input format")
- report = generate_report(data)
- return report
+ payload = await request.json()
+ validated_payload = _validate_payload(payload)
+ summary = generate_report(validated_payload)
+ return ReportSummary(
+ count=int(summary["count"]),
+ mean=float(summary["mean"]),
+ median=float(summary["median"]),
+ min=float(summary["min"]),
+ max=float(summary["max"]),
+ std_dev=float(summary["std_dev"]),
+ percentile_10=float(summary["percentile_10"]),
+ percentile_90=float(summary["percentile_90"]),
+ )
diff --git a/routes/scenarios.py b/routes/scenarios.py
index 1619b76..a1a82e8 100644
--- a/routes/scenarios.py
+++ b/routes/scenarios.py
@@ -37,13 +37,16 @@ def get_db():
@router.post("/", response_model=ScenarioRead)
def create_scenario(scenario: ScenarioCreate, db: Session = Depends(get_db)):
+ print(f"Creating scenario with name: {scenario.name}")
db_s = db.query(Scenario).filter(Scenario.name == scenario.name).first()
if db_s:
+ print(f"Scenario with name {scenario.name} already exists.")
raise HTTPException(status_code=400, detail="Scenario already exists")
new_s = Scenario(name=scenario.name, description=scenario.description)
db.add(new_s)
db.commit()
db.refresh(new_s)
+ print(f"Scenario with name {scenario.name} created successfully.")
return new_s
diff --git a/routes/ui.py b/routes/ui.py
index e69fecc..b67b4af 100644
--- a/routes/ui.py
+++ b/routes/ui.py
@@ -18,9 +18,3 @@ async def scenario_form(request: Request):
async def parameter_form(request: Request):
"""Render the parameter input form."""
return templates.TemplateResponse("ParameterInput.html", {"request": request})
-
-
-@router.get("/", response_class=HTMLResponse)
-async def dashboard(request: Request):
- """Render the central dashboard page."""
- return templates.TemplateResponse("Dashboard.html", {"request": request})
diff --git a/services/reporting.py b/services/reporting.py
index 1e6a9ce..6d7c21f 100644
--- a/services/reporting.py
+++ b/services/reporting.py
@@ -1,15 +1,57 @@
-from typing import List, Dict
+from statistics import mean, median, pstdev
+from typing import Dict, Iterable, List, Union
-def generate_report(simulation_results: List[Dict[str, float]]) -> Dict[str, float]:
- """
- Generate summary report from simulation results.
+def _extract_results(simulation_results: Iterable[Dict[str, float]]) -> List[float]:
+ values: List[float] = []
+ for item in simulation_results:
+ if not isinstance(item, dict):
+ continue
+ value = item.get("result")
+ if isinstance(value, (int, float)):
+ values.append(float(value))
+ return values
- Args:
- simulation_results: List of dicts with 'iteration' and 'result'.
- Returns:
- Dictionary with summary statistics (e.g., mean, median).
- """
- # TODO: implement reporting logic (e.g., calculate mean, median, percentiles)
- return {}
+def _percentile(values: List[float], percentile: float) -> float:
+ if not values:
+ return 0.0
+ sorted_values = sorted(values)
+ if len(sorted_values) == 1:
+ return sorted_values[0]
+ index = (percentile / 100) * (len(sorted_values) - 1)
+ lower = int(index)
+ upper = min(lower + 1, len(sorted_values) - 1)
+ weight = index - lower
+ return sorted_values[lower] * (1 - weight) + sorted_values[upper] * weight
+
+
+def generate_report(simulation_results: List[Dict[str, float]]) -> Dict[str, Union[float, int]]:
+ """Aggregate basic statistics for simulation outputs."""
+
+ values = _extract_results(simulation_results)
+
+ if not values:
+ return {
+ "count": 0,
+ "mean": 0.0,
+ "median": 0.0,
+ "min": 0.0,
+ "max": 0.0,
+ "std_dev": 0.0,
+ "percentile_10": 0.0,
+ "percentile_90": 0.0,
+ }
+
+ summary: Dict[str, Union[float, int]] = {
+ "count": len(values),
+ "mean": mean(values),
+ "median": median(values),
+ "min": min(values),
+ "max": max(values),
+ "percentile_10": _percentile(values, 10),
+ "percentile_90": _percentile(values, 90),
+ }
+
+ summary["std_dev"] = pstdev(values) if len(values) > 1 else 0.0
+ return summary
diff --git a/templates/Dashboard.html b/templates/Dashboard.html
index d4f06bf..9c166d7 100644
--- a/templates/Dashboard.html
+++ b/templates/Dashboard.html
@@ -1,25 +1,240 @@
-
-
+
+
CalMiner Dashboard
-
-
+
+
+
Simulation Results Dashboard
-
+
+
Summary Statistics
+
+
+
+
+
Sample Results Input
+
+ Provide simulation outputs as JSON (array of objects containing the
+ result field) and refresh the dashboard to preview metrics.
+