From df814736da704c7f6b730cc347cc2bace79ac71c Mon Sep 17 00:00:00 2001 From: Aadyant-7 Date: Tue, 12 May 2026 16:46:51 +0530 Subject: [PATCH 1/2] Level 6 submission --- submissions/aadyant-sood/level-6/.env.example | 3 + submissions/aadyant-sood/level-6/.gitignore | 3 + .../aadyant-sood/level-6/DASHBOARD_URL.txt | 1 + submissions/aadyant-sood/level-6/README.md | 108 +++++ submissions/aadyant-sood/level-6/app.py | 395 ++++++++++++++++++ .../aadyant-sood/level-6/factory_capacity.csv | 9 + .../level-6/factory_production.csv | 69 +++ .../aadyant-sood/level-6/factory_workers.csv | 15 + .../aadyant-sood/level-6/requirements.txt | 48 +++ .../aadyant-sood/level-6/seed_graph.py | 291 +++++++++++++ 10 files changed, 942 insertions(+) create mode 100644 submissions/aadyant-sood/level-6/.env.example create mode 100644 submissions/aadyant-sood/level-6/.gitignore create mode 100644 submissions/aadyant-sood/level-6/DASHBOARD_URL.txt create mode 100644 submissions/aadyant-sood/level-6/README.md create mode 100644 submissions/aadyant-sood/level-6/app.py create mode 100644 submissions/aadyant-sood/level-6/factory_capacity.csv create mode 100644 submissions/aadyant-sood/level-6/factory_production.csv create mode 100644 submissions/aadyant-sood/level-6/factory_workers.csv create mode 100644 submissions/aadyant-sood/level-6/requirements.txt create mode 100644 submissions/aadyant-sood/level-6/seed_graph.py diff --git a/submissions/aadyant-sood/level-6/.env.example b/submissions/aadyant-sood/level-6/.env.example new file mode 100644 index 000000000..5aaa1391d --- /dev/null +++ b/submissions/aadyant-sood/level-6/.env.example @@ -0,0 +1,3 @@ +NEO4J_URI=neo4j+ssc://your-instance.databases.neo4j.io +NEO4J_USERNAME=your_username +NEO4J_PASSWORD=your_password \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/.gitignore b/submissions/aadyant-sood/level-6/.gitignore new file mode 100644 index 000000000..9204635c2 --- /dev/null +++ b/submissions/aadyant-sood/level-6/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt b/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt new file mode 100644 index 000000000..8167e1e04 --- /dev/null +++ b/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt @@ -0,0 +1 @@ +https://lpi-developer-kit-dwxwr5ucn5qfjrzyddsefm.streamlit.app/ \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/README.md b/submissions/aadyant-sood/level-6/README.md new file mode 100644 index 000000000..9b345d870 --- /dev/null +++ b/submissions/aadyant-sood/level-6/README.md @@ -0,0 +1,108 @@ +# Factory Knowledge Graph Dashboard + +## Overview + +A Neo4j + Streamlit dashboard for factory production planning and workforce analysis using a knowledge graph model. + +The system represents factory operations as interconnected graph entities to support operational monitoring, capacity planning, staffing analysis, and forecasting. + +--- + +## Tech Stack + +- Python +- Neo4j AuraDB +- Streamlit +- Pandas +- Plotly +- NumPy +- python-dotenv + +--- + +## Knowledge Graph Model + +### Node Labels +- Project +- Product +- Station +- Worker +- Week +- Etapp +- BOP +- Certification +- Capacity + +### Relationship Types +- PRODUCES +- SCHEDULED_AT +- OCCURS_IN +- PART_OF +- BELONGS_TO +- WORKS_AT +- CAN_COVER +- HAS_CERTIFICATION +- HAS_CAPACITY + +--- + +## Dashboard Features + +### Project Overview +Project-wise planned vs actual production hour comparison with variance analysis. + +### Station Load +Station workload monitoring with overloaded station detection. + +### Capacity Tracker +Weekly capacity vs planned demand analysis with deficit visualization. + +### Worker Coverage +Cross-station worker coverage analysis with single point of failure detection. + +### Production Forecast (Bonus) +Trend-based workload forecasting with confidence bands for future planning. + +### Self-Test +Automated Neo4j validation checks with scoring out of 20. + +--- + +## Local Setup + +### Install dependencies +```bash +pip install -r requirements.txt +``` + +### Configure environment variables +Create a `.env` file: + +```env +NEO4J_URI=your_neo4j_uri +NEO4J_USERNAME=your_username +NEO4J_PASSWORD=your_password +``` + +### Seed the graph +```bash +python seed_graph.py +``` + +### Run dashboard +```bash +streamlit run app.py +``` + +--- + +## Deployment + +Live Dashboard: +https://lpi-developer-kit-dwxwr5ucn5qfjrzyddsefm.streamlit.app/ + +--- + +## Author + +Aadyant Sood \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/app.py b/submissions/aadyant-sood/level-6/app.py new file mode 100644 index 000000000..d0b061b32 --- /dev/null +++ b/submissions/aadyant-sood/level-6/app.py @@ -0,0 +1,395 @@ +import os +import streamlit as st +import pandas as pd +import numpy as np +import plotly.express as px +import plotly.graph_objects as go +from neo4j import GraphDatabase +from dotenv import load_dotenv + +# ========================================================== +# CONFIG +# ========================================================== + +st.set_page_config( + page_title="Factory Knowledge Graph Dashboard", + layout="wide" +) + +load_dotenv() + +# ========================================================== +# CONNECTION +# ========================================================== + +@st.cache_resource +def get_driver(): + try: + uri = st.secrets["NEO4J_URI"] + username = st.secrets["NEO4J_USERNAME"] + password = st.secrets["NEO4J_PASSWORD"] + except: + uri = os.getenv("NEO4J_URI") + username = os.getenv("NEO4J_USERNAME") + password = os.getenv("NEO4J_PASSWORD") + + return GraphDatabase.driver(uri, auth=(username, password)) + + +def run_query(cypher): + driver = get_driver() + with driver.session() as session: + result = session.run(cypher) + return [dict(r) for r in result] + + +# ========================================================== +# SIDEBAR +# ========================================================== + +st.sidebar.title("Factory Dashboard") + +page = st.sidebar.radio( + "Navigate", + [ + "Project Overview", + "Station Load", + "Capacity Tracker", + "Worker Coverage", + "Production Forecast", + "Self-Test" + ] +) + +# ========================================================== +# PROJECT OVERVIEW +# ========================================================== + +if page == "Project Overview": + + st.title("📦 Project Overview") + st.caption("Project performance across production workflow.") + + rows = run_query(""" + MATCH (p:Project)-[r:SCHEDULED_AT]->() + RETURN + p.project_name AS project, + SUM(r.planned_hours) AS planned, + SUM(r.actual_hours) AS actual + ORDER BY project + """) + + df = pd.DataFrame(rows) + + df["variance"] = ( + ((df["actual"] - df["planned"]) / df["planned"]) * 100 + ).round(2) + + c1, c2, c3 = st.columns(3) + + c1.metric("Projects", len(df)) + c2.metric("Total Planned Hours", int(df["planned"].sum())) + c3.metric("Total Actual Hours", int(df["actual"].sum())) + + fig = px.bar( + df, + x="project", + y=["planned", "actual"], + barmode="group", + title="Planned vs Actual Hours by Project" + ) + + st.plotly_chart(fig, use_container_width=True) + st.dataframe(df, use_container_width=True) + +# ========================================================== +# STATION LOAD +# ========================================================== + +elif page == "Station Load": + + st.title("🏭 Station Load") + st.caption("Detect overloaded stations.") + + rows = run_query(""" + MATCH (:Project)-[r:SCHEDULED_AT]->(s:Station) + RETURN + s.station_name AS station, + r.week AS week, + SUM(r.planned_hours) AS planned, + SUM(r.actual_hours) AS actual + """) + + df = pd.DataFrame(rows) + + df["status"] = np.where( + df["actual"] > df["planned"], + "Overloaded", + "Normal" + ) + + fig = px.bar( + df, + x="station", + y="actual", + color="status", + hover_data=["planned", "week"], + title="Station Load Analysis" + ) + + st.plotly_chart(fig, use_container_width=True) + + overloaded = df[df["status"] == "Overloaded"] + + if not overloaded.empty: + st.warning("Overloaded stations detected") + st.dataframe(overloaded, use_container_width=True) + +# ========================================================== +# CAPACITY TRACKER +# ========================================================== + +elif page == "Capacity Tracker": + + st.title("📈 Capacity Tracker") + st.caption("Weekly workforce capacity vs production demand.") + + rows = run_query(""" + MATCH (w:Week)-[r:HAS_CAPACITY]->() + RETURN + w.week AS week, + r.total_capacity AS capacity, + r.total_planned AS planned, + r.deficit AS deficit + ORDER BY week + """) + + df = pd.DataFrame(rows) + + fig1 = px.line( + df, + x="week", + y=["capacity", "planned"], + markers=True, + title="Capacity vs Planned Work" + ) + + st.plotly_chart(fig1, use_container_width=True) + + fig2 = px.bar( + df, + x="week", + y="deficit", + color="deficit", + title="Weekly Deficit / Surplus" + ) + + st.plotly_chart(fig2, use_container_width=True) + + risk = df[df["deficit"] < 0] + + if not risk.empty: + st.error("Capacity deficit weeks found") + st.dataframe(risk, use_container_width=True) + +# ========================================================== +# WORKER COVERAGE +# ========================================================== + +elif page == "Worker Coverage": + + st.title("👷 Worker Coverage") + st.caption("Backup staffing analysis.") + + rows = run_query(""" + MATCH (w:Worker)-[:CAN_COVER]->(s:Station) + RETURN + s.station_name AS station, + collect(w.name) AS workers, + count(w) AS coverage + ORDER BY coverage ASC + """) + + df = pd.DataFrame(rows) + + df["risk"] = np.where( + df["coverage"] == 1, + "Single Point of Failure", + "Healthy" + ) + + fig = px.bar( + df, + x="station", + y="coverage", + color="risk", + title="Coverage by Station" + ) + + st.plotly_chart(fig, use_container_width=True) + + spof = df[df["coverage"] == 1] + + if not spof.empty: + st.warning("Single Point of Failure detected") + st.dataframe(spof, use_container_width=True) + + st.dataframe(df, use_container_width=True) + +# ========================================================== +# BONUS FORECAST +# ========================================================== + +elif page == "Production Forecast": + + st.title("🔮 Production Forecast") + st.caption("Trend-based production load forecast.") + + rows = run_query(""" + MATCH (:Project)-[r:SCHEDULED_AT]->(s:Station) + RETURN + s.station_name AS station, + r.week AS week, + SUM(r.actual_hours) AS actual + """) + + df = pd.DataFrame(rows) + + df["week_num"] = df["week"].str.replace("w", "").astype(int) + + forecast_rows = [] + + for station in df["station"].unique(): + + temp = df[df["station"] == station].sort_values("week_num") + + if len(temp) < 2: + continue + + x = temp["week_num"].values + y = temp["actual"].values + + slope, intercept = np.polyfit(x, y, 1) + + predicted = slope * 9 + intercept + + residuals = y - (slope * x + intercept) + uncertainty = np.std(residuals) + + forecast_rows.append({ + "station": station, + "forecast": round(predicted, 2), + "upper": round(predicted + uncertainty, 2), + "lower": round(predicted - uncertainty, 2) + }) + + forecast_df = pd.DataFrame(forecast_rows) + + fig = go.Figure() + + fig.add_trace(go.Bar( + x=forecast_df["station"], + y=forecast_df["forecast"], + name="Forecast" + )) + + fig.add_trace(go.Scatter( + x=forecast_df["station"], + y=forecast_df["upper"], + mode="lines", + name="Upper Bound" + )) + + fig.add_trace(go.Scatter( + x=forecast_df["station"], + y=forecast_df["lower"], + mode="lines", + fill="tonexty", + name="Confidence Band" + )) + + st.plotly_chart(fig, use_container_width=True) + st.dataframe(forecast_df, use_container_width=True) + +# ========================================================== +# SELF TEST +# ========================================================== + +elif page == "Self-Test": + + st.title("✅ Self-Test") + st.caption("Automated graph validation.") + + def self_test(): + + score = 0 + checks = [] + + driver = get_driver() + + try: + with driver.session() as s: + s.run("RETURN 1") + checks.append(("Neo4j connection", True, 3)) + score += 3 + except: + checks.append(("Neo4j connection", False, 3)) + return checks, score + + with driver.session() as s: + + node_count = s.run( + "MATCH (n) RETURN count(n) AS c" + ).single()["c"] + + checks.append(("Node count", node_count >= 50, 3)) + if node_count >= 50: + score += 3 + + rel_count = s.run( + "MATCH ()-[r]->() RETURN count(r) AS c" + ).single()["c"] + + checks.append(("Relationship count", rel_count >= 100, 3)) + if rel_count >= 100: + score += 3 + + labels = s.run( + "CALL db.labels() YIELD label RETURN count(label) AS c" + ).single()["c"] + + checks.append(("Node labels", labels >= 6, 3)) + if labels >= 6: + score += 3 + + rel_types = s.run( + "CALL db.relationshipTypes() YIELD relationshipType RETURN count(relationshipType) AS c" + ).single()["c"] + + checks.append(("Relationship types", rel_types >= 8, 3)) + if rel_types >= 8: + score += 3 + + variance = list(s.run(""" + MATCH (p:Project)-[r:SCHEDULED_AT]->(s:Station) + WHERE r.actual_hours > r.planned_hours * 1.1 + RETURN p.project_name, s.station_name + LIMIT 10 + """)) + + checks.append(("Variance query", len(variance) > 0, 5)) + if len(variance) > 0: + score += 5 + + return checks, score + + checks, total = self_test() + + for label, passed, pts in checks: + if passed: + st.success(f"{label} ✅ ({pts}/{pts})") + else: + st.error(f"{label} ❌ (0/{pts})") + + st.divider() + st.subheader(f"SELF-TEST SCORE: {total}/20") \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/factory_capacity.csv b/submissions/aadyant-sood/level-6/factory_capacity.csv new file mode 100644 index 000000000..795ff52f0 --- /dev/null +++ b/submissions/aadyant-sood/level-6/factory_capacity.csv @@ -0,0 +1,9 @@ +week,own_staff_count,hired_staff_count,own_hours,hired_hours,overtime_hours,total_capacity,total_planned,deficit +w1,10,2,400,80,0,480,612,-132 +w2,10,2,400,80,40,520,645,-125 +w3,10,2,400,80,0,480,398,82 +w4,10,2,400,80,20,500,550,-50 +w5,10,2,400,80,30,510,480,30 +w6,9,2,360,80,0,440,520,-80 +w7,10,2,400,80,40,520,600,-80 +w8,10,2,400,80,20,500,470,30 \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/factory_production.csv b/submissions/aadyant-sood/level-6/factory_production.csv new file mode 100644 index 000000000..ca6ce43e1 --- /dev/null +++ b/submissions/aadyant-sood/level-6/factory_production.csv @@ -0,0 +1,69 @@ +project_id,project_number,project_name,product_type,unit,quantity,unit_factor,station_code,station_name,etapp,bop,week,planned_hours,actual_hours,completed_units +P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w1,48.0,45.2,28 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w1,32.0,35.5,25 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,013,Montering IQB,ET1,BOP1,w1,28.0,26.0,22 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,014,Svets o montage IQB,ET1,BOP1,w1,35.0,38.2,20 +P01,4501,Stålverket Borås,SB,styck,40,4.0,018,SB B/F-hall,ET1,BOP1,w1,16.0,14.5,4 +P01,4501,Stålverket Borås,SP,styck,180,2.0,019,SP B/F-hall,ET1,BOP1,w1,12.0,13.0,7 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,011,FS IQB,ET1,BOP1,w2,48.0,50.0,32 +P01,4501,Stålverket Borås,IQB,meter,600,1.77,012,Förmontering IQB,ET1,BOP1,w2,32.0,30.0,28 +P01,4501,Stålverket Borås,IQP,styck,90,2.80,015,Montering IQP,ET1,BOP2,w2,25.0,28.0,9 +P01,4501,Stålverket Borås,SR,styck,8,45.0,021,SR B/F-hall,ET1,BOP2,w2,40.0,42.0,1 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w1,30.0,28.0,20 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,012,Förmontering IQB,ET1,BOP1,w1,22.0,24.5,18 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,013,Montering IQB,ET1,BOP1,w1,18.0,17.0,16 +P02,4502,Kontorshus Mölndal,IQP,styck,70,2.70,015,Montering IQP,ET1,BOP1,w1,19.0,21.0,7 +P02,4502,Kontorshus Mölndal,SD,styck,30,3.00,018,SB B/F-hall,ET1,BOP1,w1,9.0,8.5,3 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,011,FS IQB,ET1,BOP1,w2,30.0,32.0,24 +P02,4502,Kontorshus Mölndal,IQB,meter,350,1.50,014,Svets o montage IQB,ET1,BOP1,w2,25.0,23.0,20 +P02,4502,Kontorshus Mölndal,SP,styck,120,1.75,019,SP B/F-hall,ET1,BOP2,w2,14.0,15.5,8 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w1,72.0,70.0,40 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,012,Förmontering IQB,ET1,BOP1,w1,48.0,52.0,35 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,013,Montering IQB,ET1,BOP1,w1,38.0,36.5,30 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,014,Svets o montage IQB,ET1,BOP1,w1,42.0,48.0,28 +P03,4503,Lagerhall Jönköping,SB,styck,60,6.00,018,SB B/F-hall,ET1,BOP1,w1,36.0,38.0,6 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,011,FS IQB,ET1,BOP1,w2,72.0,75.0,45 +P03,4503,Lagerhall Jönköping,IQP,styck,110,2.90,015,Montering IQP,ET1,BOP2,w2,32.0,30.0,11 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,016,Gjutning,ET1,BOP2,w2,28.0,35.0,8 +P03,4503,Lagerhall Jönköping,IQB,meter,900,1.89,017,Målning,ET1,BOP2,w3,24.0,22.0,20 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w1,38.0,36.0,24 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,012,Förmontering IQB,ET1,BOP1,w1,25.0,27.0,20 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,013,Montering IQB,ET1,BOP1,w1,20.0,19.0,18 +P04,4504,Parkering Helsingborg,IQP,styck,55,2.85,015,Montering IQP,ET1,BOP1,w1,16.0,18.0,6 +P04,4504,Parkering Helsingborg,SB,styck,25,7.50,018,SB B/F-hall,ET1,BOP1,w1,19.0,22.0,3 +P04,4504,Parkering Helsingborg,IQB,meter,450,1.65,011,FS IQB,ET1,BOP1,w2,38.0,40.0,28 +P04,4504,Parkering Helsingborg,SP,styck,100,2.00,019,SP B/F-hall,ET1,BOP2,w2,12.0,11.0,6 +P04,4504,Parkering Helsingborg,SR,styck,12,120.0,021,SR B/F-hall,ET1,BOP2,w2,60.0,65.0,1 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w1,95.0,90.0,50 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,012,Förmontering IQB,ET2,BOP3,w1,65.0,68.0,42 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,013,Montering IQB,ET2,BOP3,w1,50.0,48.0,38 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,014,Svets o montage IQB,ET2,BOP3,w1,58.0,62.0,35 +P05,4505,Sjukhus Linköping ET2,IQP,styck,150,2.88,015,Montering IQP,ET2,BOP3,w1,30.0,33.0,10 +P05,4505,Sjukhus Linköping ET2,SB,styck,50,5.00,018,SB B/F-hall,ET2,BOP3,w1,25.0,28.0,5 +P05,4505,Sjukhus Linköping ET2,SD,styck,45,2.75,018,SB B/F-hall,ET2,BOP3,w1,12.0,11.5,4 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,011,FS IQB,ET2,BOP3,w2,95.0,98.0,55 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,016,Gjutning,ET2,BOP3,w2,35.0,40.0,12 +P05,4505,Sjukhus Linköping ET2,IQB,meter,1200,1.85,017,Målning,ET2,BOP3,w2,28.0,26.0,25 +P05,4505,Sjukhus Linköping ET2,SR,styck,20,274.0,021,SR B/F-hall,ET2,BOP3,w3,120.0,115.0,2 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,011,FS IQB,ET1,BOP1,w2,40.0,38.0,26 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,012,Förmontering IQB,ET1,BOP1,w2,28.0,30.0,22 +P06,4506,Skola Uppsala,IQB,meter,500,1.60,013,Montering IQB,ET1,BOP1,w2,22.0,20.0,18 +P06,4506,Skola Uppsala,IQP,styck,80,2.75,015,Montering IQP,ET1,BOP1,w2,22.0,24.0,8 +P06,4506,Skola Uppsala,SB,styck,35,4.50,018,SB B/F-hall,ET1,BOP1,w2,16.0,18.0,4 +P06,4506,Skola Uppsala,SP,styck,140,1.50,019,SP B/F-hall,ET1,BOP2,w3,14.0,12.0,10 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w1,45.0,42.0,22 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,012,Förmontering IQB,ET1,BOP1,w1,30.0,33.0,18 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,014,Svets o montage IQB,ET1,BOP1,w1,35.0,32.0,16 +P07,4507,Idrottshall Västerås,SB,styck,45,3.50,018,SB B/F-hall,ET1,BOP1,w1,16.0,18.0,5 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,011,FS IQB,ET1,BOP1,w2,45.0,48.0,26 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,016,Gjutning,ET1,BOP2,w2,20.0,22.0,5 +P07,4507,Idrottshall Västerås,HSQ,meter,400,2.05,017,Målning,ET1,BOP2,w3,18.0,16.0,15 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w1,65.0,62.0,36 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,012,Förmontering IQB,ET1,BOP1,w1,42.0,45.0,30 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,013,Montering IQB,ET1,BOP1,w1,35.0,38.0,25 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,014,Svets o montage IQB,ET1,BOP1,w1,40.0,44.0,22 +P08,4508,Bro E6 Halmstad,SP,styck,200,2.50,019,SP B/F-hall,ET1,BOP1,w1,20.0,18.0,8 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,011,FS IQB,ET1,BOP1,w2,65.0,68.0,42 +P08,4508,Bro E6 Halmstad,IQP,styck,95,2.93,015,Montering IQP,ET1,BOP2,w2,28.0,30.0,10 +P08,4508,Bro E6 Halmstad,IQB,meter,800,1.80,016,Gjutning,ET1,BOP2,w3,22.0,25.0,8 +P08,4508,Bro E6 Halmstad,SR,styck,15,180.0,021,SR B/F-hall,ET1,BOP2,w3,90.0,85.0,2 \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/factory_workers.csv b/submissions/aadyant-sood/level-6/factory_workers.csv new file mode 100644 index 000000000..3110285cc --- /dev/null +++ b/submissions/aadyant-sood/level-6/factory_workers.csv @@ -0,0 +1,15 @@ +worker_id,name,role,primary_station,can_cover_stations,certifications,hours_per_week,type +W01,Erik Lindberg,Operator,011,"011,012","MIG/MAG,TIG,ISO 9606",40,permanent +W02,Anna Berg,Operator,011,"011,014","MIG/MAG,TIG",40,permanent +W03,Lars Jensen,Operator,012,"012,013","Surface treatment,CE marking",40,permanent +W04,Maria Stone,Operator,013,"013","Blasting,Surface protection",40,permanent +W05,Johan Peters,Operator,014,"014,015","Hydraulics,Mechanics,Crane",40,permanent +W06,Karen Nilsen,Inspector,015,"015","SIS,SS-EN 1090,NDT",40,permanent +W07,Per Hansen,Operator,016,"016,017","Casting,Formwork",40,permanent +W08,Sofia Arden,Operator,017,"017","Surface treatment,Spray painting",40,permanent +W09,Magnus Stone,Operator,018,"018,019","Sheet metal,Assembly",40,permanent +W10,Elin Frank,Operator,019,"019,018","Assembly,Welding",32,permanent +W11,Victor Elm,Foreman,all,"011,012,013,014,015,016,017,018,019,021","Leadership,CE,ISO 9001",45,permanent +W12,Lena Dale,Quality Manager,015,"015","ISO 9001,SS-EN 1090,Audit",40,permanent +W13,Ahmed Hassan,Operator,011,"011","MIG/MAG",40,hired +W14,Petra Steen,Operator,012,"012,013","Surface treatment",40,hired \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/requirements.txt b/submissions/aadyant-sood/level-6/requirements.txt new file mode 100644 index 000000000..bf83b47d7 --- /dev/null +++ b/submissions/aadyant-sood/level-6/requirements.txt @@ -0,0 +1,48 @@ +altair==6.1.0 +anyio==4.13.0 +attrs==26.1.0 +blinker==1.9.0 +cachetools==7.1.1 +certifi==2026.4.22 +charset-normalizer==3.4.7 +click==8.3.3 +colorama==0.4.6 +gitdb==4.0.12 +GitPython==3.1.50 +h11==0.16.0 +httptools==0.7.1 +idna==3.13 +itsdangerous==2.2.0 +Jinja2==3.1.6 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +MarkupSafe==3.0.3 +narwhals==2.21.0 +neo4j==6.2.0 +numpy==2.4.4 +packaging==26.2 +pandas==3.0.2 +pillow==12.2.0 +plotly==6.7.0 +protobuf==7.34.1 +pyarrow==24.0.0 +pydeck==0.9.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.2 +python-multipart==0.0.28 +pytz==2026.2 +referencing==0.37.0 +requests==2.33.1 +rpds-py==0.30.0 +six==1.17.0 +smmap==5.0.3 +starlette==1.0.0 +streamlit==1.57.0 +tenacity==9.1.4 +toml==0.10.2 +typing_extensions==4.15.0 +tzdata==2026.2 +urllib3==2.7.0 +uvicorn==0.46.0 +watchdog==6.0.0 +websockets==16.0 \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/seed_graph.py b/submissions/aadyant-sood/level-6/seed_graph.py new file mode 100644 index 000000000..3d8bbc50f --- /dev/null +++ b/submissions/aadyant-sood/level-6/seed_graph.py @@ -0,0 +1,291 @@ +from neo4j import GraphDatabase +from dotenv import load_dotenv +import pandas as pd +import os + +load_dotenv(".env") + +URI = os.getenv("NEO4J_URI") +USERNAME = os.getenv("NEO4J_USERNAME") +PASSWORD = os.getenv("NEO4J_PASSWORD") + +driver = GraphDatabase.driver( + URI, + auth=(USERNAME, PASSWORD) +) + +production_df = pd.read_csv("factory_production.csv", dtype={"station_code": str}) +workers_df = pd.read_csv("factory_workers.csv", dtype={"primary_station": str}) +capacity_df = pd.read_csv("factory_capacity.csv") + + +with driver.session() as session: + + # ========================= + # CONSTRAINTS + # ========================= + + constraints = [ + ("Project", "project_id"), + ("Product", "product_type"), + ("Station", "station_code"), + ("Worker", "worker_id"), + ("Week", "week"), + ] + + for label, prop in constraints: + session.run(f""" + CREATE CONSTRAINT IF NOT EXISTS + FOR (n:{label}) + REQUIRE n.{prop} IS UNIQUE + """) + + # ========================= + # PROJECTS + # ========================= + + for _, row in production_df.iterrows(): + session.run(""" + MERGE (p:Project {project_id: $project_id}) + SET p.project_number = $project_number, + p.project_name = $project_name + """, + project_id=row["project_id"], + project_number=row["project_number"], + project_name=row["project_name"] + ) + + # ========================= + # PRODUCTS + # ========================= + + for _, row in production_df.iterrows(): + session.run(""" + MERGE (p:Product {product_type: $product_type}) + SET p.unit = $unit + """, + product_type=row["product_type"], + unit=row["unit"] + ) + + # ========================= + # STATIONS + # ========================= + + for _, row in production_df.iterrows(): + session.run(""" + MERGE (s:Station {station_code: $station_code}) + SET s.station_name = $station_name + """, + station_code=row["station_code"], + station_name=row["station_name"] + ) + + # ========================= + # WEEKS + # ========================= + + for week in production_df["week"].unique(): + session.run(""" + MERGE (w:Week {week: $week}) + """, week=week) + + # ========================= + # ETAPP + # ========================= + + for etapp in production_df["etapp"].unique(): + session.run(""" + MERGE (e:Etapp {name: $etapp}) + """, etapp=etapp) + + # ========================= + # BOP + # ========================= + + for bop in production_df["bop"].unique(): + session.run(""" + MERGE (b:BOP {name: $bop}) + """, bop=bop) + + # ========================= + # WORKERS + # ========================= + + for _, row in workers_df.iterrows(): + session.run(""" + MERGE (w:Worker {worker_id: $worker_id}) + SET w.name = $name, + w.role = $role, + w.type = $type, + w.hours_per_week = $hours_per_week + """, + worker_id=row["worker_id"], + name=row["name"], + role=row["role"], + type=row["type"], + hours_per_week=row["hours_per_week"] + ) + + # ========================= + # CERTIFICATIONS + # ========================= + + certs = set() + + for cert_list in workers_df["certifications"]: + for cert in str(cert_list).split(","): + certs.add(cert.strip()) + + for cert in certs: + session.run(""" + MERGE (c:Certification {name: $cert}) + """, cert=cert) + + # ========================= + # CAPACITY + # ========================= + + for _, row in capacity_df.iterrows(): + session.run(""" + MERGE (c:Capacity {week: $week}) + SET c.own_staff_count = $own_staff_count, + c.hired_staff_count = $hired_staff_count + """, + week=row["week"], + own_staff_count=row["own_staff_count"], + hired_staff_count=row["hired_staff_count"] + ) + + # ========================= + # RELATIONSHIPS + # ========================= + + for _, row in production_df.iterrows(): + + session.run(""" + MATCH (p:Project {project_id: $project_id}) + MATCH (pr:Product {product_type: $product_type}) + MERGE (p)-[r:PRODUCES]->(pr) + SET r.quantity = $quantity, + r.unit_factor = $unit_factor + """, + project_id=row["project_id"], + product_type=row["product_type"], + quantity=row["quantity"], + unit_factor=row["unit_factor"] + ) + + session.run(""" + MATCH (p:Project {project_id: $project_id}) + MATCH (s:Station {station_code: $station_code}) + MERGE (p)-[r:SCHEDULED_AT {week: $week}]->(s) + SET r.planned_hours = $planned_hours, + r.actual_hours = $actual_hours, + r.completed_units = $completed_units + """, + project_id=row["project_id"], + station_code=row["station_code"], + week=row["week"], + planned_hours=row["planned_hours"], + actual_hours=row["actual_hours"], + completed_units=row["completed_units"] + ) + + session.run(""" + MATCH (p:Project {project_id: $project_id}) + MATCH (w:Week {week: $week}) + MERGE (p)-[:OCCURS_IN]->(w) + """, + project_id=row["project_id"], + week=row["week"] + ) + + session.run(""" + MATCH (p:Project {project_id: $project_id}) + MATCH (e:Etapp {name: $etapp}) + MERGE (p)-[:PART_OF]->(e) + """, + project_id=row["project_id"], + etapp=row["etapp"] + ) + + session.run(""" + MATCH (p:Project {project_id: $project_id}) + MATCH (b:BOP {name: $bop}) + MERGE (p)-[:BELONGS_TO]->(b) + """, + project_id=row["project_id"], + bop=row["bop"] + ) + + # ========================= + # WORKER RELATIONSHIPS + # ========================= + + for _, row in workers_df.iterrows(): + + primary = str(row["primary_station"]).zfill(3) + + if primary.lower() != "all": + session.run(""" + MATCH (w:Worker {worker_id: $worker_id}) + MATCH (s:Station {station_code: $station}) + MERGE (w)-[:WORKS_AT]->(s) + """, + worker_id=row["worker_id"], + station=primary + ) + + for station in str(row["can_cover_stations"]).split(","): + code = station.strip() + + if code.isdigit(): + code = code.zfill(3) + + session.run(""" + MATCH (w:Worker {worker_id: $worker_id}) + MATCH (s:Station {station_code: $station}) + MERGE (w)-[:CAN_COVER]->(s) + """, + worker_id=row["worker_id"], + station=code + ) + + for cert in str(row["certifications"]).split(","): + session.run(""" + MATCH (w:Worker {worker_id: $worker_id}) + MATCH (c:Certification {name: $cert}) + MERGE (w)-[:HAS_CERTIFICATION]->(c) + """, + worker_id=row["worker_id"], + cert=cert.strip() + ) + + # ========================= + # CAPACITY RELATIONSHIPS + # ========================= + + for _, row in capacity_df.iterrows(): + session.run(""" + MATCH (w:Week {week: $week}) + MATCH (c:Capacity {week: $week}) + MERGE (w)-[r:HAS_CAPACITY]->(c) + SET r.own_hours = $own_hours, + r.hired_hours = $hired_hours, + r.overtime_hours = $overtime_hours, + r.total_capacity = $total_capacity, + r.total_planned = $total_planned, + r.deficit = $deficit + """, + week=row["week"], + own_hours=row["own_hours"], + hired_hours=row["hired_hours"], + overtime_hours=row["overtime_hours"], + total_capacity=row["total_capacity"], + total_planned=row["total_planned"], + deficit=row["deficit"] + ) + +print("Knowledge Graph Created Successfully!") +driver.close() \ No newline at end of file From b0592c1564d7b0c0b2d735680dc891468a6d783a Mon Sep 17 00:00:00 2001 From: Aadyant-7 Date: Tue, 12 May 2026 17:07:25 +0530 Subject: [PATCH 2/2] Update deployed dashboard URL --- submissions/aadyant-sood/level-6/DASHBOARD_URL.txt | 2 +- submissions/aadyant-sood/level-6/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt b/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt index 8167e1e04..e9ebb5821 100644 --- a/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt +++ b/submissions/aadyant-sood/level-6/DASHBOARD_URL.txt @@ -1 +1 @@ -https://lpi-developer-kit-dwxwr5ucn5qfjrzyddsefm.streamlit.app/ \ No newline at end of file +https://lpi-developer-kit-exok2achm8rvscbqwf4gyc.streamlit.app/ \ No newline at end of file diff --git a/submissions/aadyant-sood/level-6/README.md b/submissions/aadyant-sood/level-6/README.md index 9b345d870..407398098 100644 --- a/submissions/aadyant-sood/level-6/README.md +++ b/submissions/aadyant-sood/level-6/README.md @@ -99,7 +99,7 @@ streamlit run app.py ## Deployment Live Dashboard: -https://lpi-developer-kit-dwxwr5ucn5qfjrzyddsefm.streamlit.app/ +https://lpi-developer-kit-exok2achm8rvscbqwf4gyc.streamlit.app/ ---