Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
545 changes: 545 additions & 0 deletions submissions/saima-afroz/level5/answers.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions submissions/saima-afroz/level5/schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Factory Graph Schema — Saima Afroz

See answers.md Q1 for full written explanation.

graph TD
subgraph Canopy
Project -->|PRODUCES qty,unit_factor| Product
Project -->|SCHEDULED_AT planned_h,actual_h,week| Station
Project -->|BELONGS_TO| Etapp
Etapp -->|CONTAINS_BOP| BOP
BOP -->|SPANS_WEEK| Week
Station -->|ACTIVE_IN| Week
end
subgraph Geography
Factory -->|LOCATED_IN| Country
Station -->|IN_FACTORY| Factory
Worker -->|EMPLOYED_IN| Country
end
subgraph Underground
Worker -->|WORKS_AT| Station
Worker -->|CAN_COVER valid_in_country,cert_valid,travel_days| Station
Alert -->|TRIGGERED_BY variance_pct| Station
Alert -->|AFFECTS| Project
end
3 changes: 3 additions & 0 deletions submissions/saima-afroz/level6/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
NEO4J_URI=neo4j+s://xxxxxxxx.databases.neo4j.io
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-password-here
1 change: 1 addition & 0 deletions submissions/saima-afroz/level6/DASHBOARD_URL.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://lpi-developer-kit-dwynwc8iz7ezj4zrpz3ris.streamlit.app
4 changes: 4 additions & 0 deletions submissions/saima-afroz/level6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Factory Graph Dashboard - Level 6

Run: python seed_graph.py
Run: streamlit run app.py
495 changes: 495 additions & 0 deletions submissions/saima-afroz/level6/app.py

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions submissions/saima-afroz/level6/factory_capacity.csv
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions submissions/saima-afroz/level6/factory_production.csv
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions submissions/saima-afroz/level6/factory_workers.csv
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions submissions/saima-afroz/level6/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
streamlit
neo4j
python-dotenv
pandas
plotly
228 changes: 228 additions & 0 deletions submissions/saima-afroz/level6/seed_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""
seed_graph.py — Populate Neo4j with factory data from 3 CSVs.
Run once: python seed_graph.py
Safe to re-run (uses MERGE everywhere — idempotent).
"""

import os
import csv
from neo4j import GraphDatabase
from dotenv import load_dotenv

load_dotenv()

URI = os.getenv("NEO4J_URI")
USER = os.getenv("NEO4J_USER", "neo4j")
PASSWORD = os.getenv("NEO4J_PASSWORD")

# ── helpers ──────────────────────────────────────────────────────────────────

def safe_float(val, default=0.0):
try:
return float(val)
except (ValueError, TypeError):
return default

def safe_int(val, default=0):
try:
return int(val)
except (ValueError, TypeError):
return default

# ── constraints ──────────────────────────────────────────────────────────────

def create_constraints(session):
constraints = [
"CREATE CONSTRAINT IF NOT EXISTS FOR (p:Project) REQUIRE p.id IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (s:Station) REQUIRE s.code IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (pr:Product) REQUIRE pr.type IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (w:Worker) REQUIRE w.id IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (wk:Week) REQUIRE wk.name IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (e:Etapp) REQUIRE e.name IS UNIQUE",
"CREATE CONSTRAINT IF NOT EXISTS FOR (b:BOP) REQUIRE b.name IS UNIQUE",
]
for c in constraints:
session.run(c)
print("✅ Constraints created")

# ── production.csv ────────────────────────────────────────────────────────────

def load_production(session, filepath):
with open(filepath, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))

for row in rows:
project_id = row["project_id"].strip()
project_num = row["project_number"].strip()
project_name = row["project_name"].strip()
product_type = row["product_type"].strip()
unit = row["unit"].strip()
quantity = safe_int(row["quantity"])
unit_factor = safe_float(row["unit_factor"])
station_code = row["station_code"].strip()
station_name = row["station_name"].strip()
etapp = row["etapp"].strip()
bop = row["bop"].strip()
week = row["week"].strip()
planned = safe_float(row["planned_hours"])
actual = safe_float(row["actual_hours"])
completed = safe_int(row["completed_units"])

session.run("""
MERGE (p:Project {id: $pid})
ON CREATE SET p.number = $pnum, p.name = $pname

MERGE (pr:Product {type: $ptype})
ON CREATE SET pr.unit = $unit

MERGE (s:Station {code: $scode})
ON CREATE SET s.name = $sname

MERGE (e:Etapp {name: $etapp})
MERGE (b:BOP {name: $bop})
MERGE (wk:Week {name: $week})

MERGE (p)-[:BELONGS_TO]->(e)
MERGE (e)-[:CONTAINS_BOP]->(b)
MERGE (b)-[:SPANS_WEEK]->(wk)

MERGE (p)-[prod:PRODUCES]->(pr)
ON CREATE SET prod.quantity = $qty, prod.unit_factor = $uf

MERGE (p)-[sched:SCHEDULED_AT {week: $week}]->(s)
ON CREATE SET
sched.planned_hours = $planned,
sched.actual_hours = $actual,
sched.completed_units = $completed,
sched.variance_pct = CASE WHEN $planned > 0
THEN round(($actual - $planned) / $planned * 100 * 10) / 10
ELSE 0 END

MERGE (s)-[:ACTIVE_IN]->(wk)
""", pid=project_id, pnum=project_num, pname=project_name,
ptype=product_type, unit=unit,
scode=station_code, sname=station_name,
etapp=etapp, bop=bop, week=week,
qty=quantity, uf=unit_factor,
planned=planned, actual=actual, completed=completed)

print(f"✅ Production loaded ({len(rows)} rows → Projects, Products, Stations, Weeks, Etapps, BOPs)")

# ── factory_workers.csv ───────────────────────────────────────────────────────

def load_workers(session, filepath):
with open(filepath, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))

for row in rows:
worker_id = row["worker_id"].strip()
name = row["name"].strip()
role = row["role"].strip()
primary = row["primary_station"].strip()
can_cover_raw = row["can_cover_stations"].strip()
certs = row["certifications"].strip()
hours_per_week = safe_int(row["hours_per_week"])
wtype = row["type"].strip()

session.run("""
MERGE (w:Worker {id: $wid})
ON CREATE SET
w.name = $name,
w.role = $role,
w.certifications = $certs,
w.hours_per_week = $hpw,
w.type = $wtype
""", wid=worker_id, name=name, role=role,
certs=certs, hpw=hours_per_week, wtype=wtype)

# primary station
if primary and primary != "all":
session.run("""
MATCH (w:Worker {id: $wid})
MERGE (s:Station {code: $scode})
MERGE (w)-[:WORKS_AT]->(s)
""", wid=worker_id, scode=primary)

# coverage stations
cover_codes = [c.strip() for c in can_cover_raw.split(",") if c.strip()]
if primary == "all":
# Victor Elm can cover all — fetch all stations
session.run("""
MATCH (w:Worker {id: $wid})
MATCH (s:Station)
MERGE (w)-[:CAN_COVER]->(s)
""", wid=worker_id)
else:
for code in cover_codes:
session.run("""
MATCH (w:Worker {id: $wid})
MERGE (s:Station {code: $scode})
MERGE (w)-[:CAN_COVER]->(s)
""", wid=worker_id, scode=code)

print(f"✅ Workers loaded ({len(rows)} workers)")

# ── factory_capacity.csv ──────────────────────────────────────────────────────

def load_capacity(session, filepath):
with open(filepath, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))

for row in rows:
week = row["week"].strip()
own_staff = safe_int(row["own_staff_count"])
hired_staff = safe_int(row["hired_staff_count"])
own_hours = safe_float(row["own_hours"])
hired_hours = safe_float(row["hired_hours"])
overtime = safe_float(row["overtime_hours"])
total_cap = safe_float(row["total_capacity"])
total_plan = safe_float(row["total_planned"])
deficit = safe_float(row["deficit"])

session.run("""
MERGE (wk:Week {name: $week})
ON MATCH SET
wk.own_staff_count = $own_staff,
wk.hired_staff_count = $hired_staff,
wk.own_hours = $own_hours,
wk.hired_hours = $hired_hours,
wk.overtime_hours = $overtime,
wk.total_capacity = $total_cap,
wk.total_planned = $total_plan,
wk.deficit = $deficit
""", week=week, own_staff=own_staff, hired_staff=hired_staff,
own_hours=own_hours, hired_hours=hired_hours, overtime=overtime,
total_cap=total_cap, total_plan=total_plan, deficit=deficit)

print(f"✅ Capacity loaded ({len(rows)} weeks)")

# ── main ──────────────────────────────────────────────────────────────────────

def main():
print("Connecting to Neo4j...")
driver = GraphDatabase.driver(URI, auth=(USER, PASSWORD))
driver.verify_connectivity()
print(f"✅ Connected to {URI}\n")

with driver.session() as session:
create_constraints(session)
load_production(session, "factory_production.csv")
load_workers(session, "factory_workers.csv")
load_capacity(session, "factory_capacity.csv")

# Summary
with driver.session() as session:
nodes = session.run("MATCH (n) RETURN count(n) AS c").single()["c"]
rels = session.run("MATCH ()-[r]->() RETURN count(r) AS c").single()["c"]
labels = session.run("CALL db.labels() YIELD label RETURN count(label) AS c").single()["c"]
rel_types = session.run("CALL db.relationshipTypes() YIELD relationshipType RETURN count(relationshipType) AS c").single()["c"]

print(f"\n📊 Graph summary:")
print(f" Nodes: {nodes}")
print(f" Relationships: {rels}")
print(f" Node labels: {labels}")
print(f" Relationship types: {rel_types}")
driver.close()

if __name__ == "__main__":
main()
Loading