--- /dev/null
+env/*
+data/*.csv
+data/*.png
\ No newline at end of file
--- /dev/null
+*.csv
+__pycache__
+*.pyc
+*.pyo
+env/
+.idea/
\ No newline at end of file
--- /dev/null
+# Leggimi
+
+Lo scopo del progetto è fornire un modello che consenta di predire il numero di ore di utilizzo di una apparecchiatura
+elettrica in un singolo giorno e di poter effettuare previsioni per i giorni successivi.
+
+## Come funziona
+
+Il modello è stato sviluppato utilizzando la libreria [TensorFlow](https://www.tensorflow.org/) e la rete neurale LSTM.
+I dataset per l'addestramento sono stati generati manualmente.
+
+Puoi sempre generare nuovi dataset utilizzando `Simulator` come nell'esempio riportato di seguito:
+
+```python
+from simulator import Simulator
+from predictor import Predictor
+
+machines = 10 # Numero delle tipologie di apparecchiature elettromedicali da simulare.
+samples = 5000 # Numero dei campioni da generare
+
+simulator = Simulator()
+train_features, train_labels = simulator.generate(machines, samples, 'data/train.csv')
+test_features, test_labels = simulator.generate(machines, samples, 'data/test.csv')
+
+Predictor.train(train_features, train_labels, test_features, test_labels, "model.h5")
+```
+
+Successivamente puoi utilizzare il modello per effettuare previsioni:
+
+```python
+from predictor import Predictor
+
+predictor = Predictor("model.h5")
+
+# Previsione per il giorno 1
+features = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+prediction = predictor.predict(features)
+print(prediction)
+predictor.plot(prediction, "day1.png")
+```
--- /dev/null
+image: google/cloud-sdk:412.0.0-slim
+definitions:
+ steps:
+ - step: &publish
+ name: "Build"
+ caches:
+ - maven
+ - pip
+ services:
+ - docker
+ script:
+ - source scripts/set-env.sh
+ - cat scripts/service-account.json.base64 | base64 --decode >> service-account.json
+ - gcloud auth activate-service-account --key-file service-account.json
+ - export VERSION=1.0.${BITBUCKET_BUILD_NUMBER}
+ - export IMAGE_BASE="eu.gcr.io/${PROJECT_ID}/${NAMESPACE}-${MODULE}"
+ - export IMAGE="${IMAGE_BASE}:${VERSION}"
+ - gcloud config set project ${PROJECT_ID}
+ - gcloud config set compute/zone europe-west1-b
+ - gcloud auth configure-docker
+ - >-
+ docker build -t ${IMAGE} --build-arg PROFILE=${PROFILE} -f ./scripts/docker/Dockerfile .
+ - docker push ${IMAGE}
+ - docker tag $IMAGE "${IMAGE_BASE}:latest"
+ - docker push "${IMAGE_BASE}:latest"
+ - git tag -a "${VERSION}" -m "version ${VERSION}"
+ - git push origin "${VERSION}"
+ - mkdir -p ./build
+ - echo $VERSION > ./build/VERSION
+ - echo $IMAGE > ./build/IMAGE
+ artifacts:
+ - build/**
+ - step: &deploy
+ name: Deploy
+ script:
+ - source scripts/set-env.sh
+ - export VERSION=$(cat ./build/VERSION)
+ - export IMAGE=$(cat ./build/IMAGE)
+ - cat scripts/service-account.json.base64 | base64 --decode >> service-account.json
+ - gcloud auth activate-service-account --key-file service-account.json
+ - gcloud config set project ${PROJECT_ID}
+ - gcloud config set compute/zone europe-west1-b
+ - apt install kubectl
+ - apt install google-cloud-sdk-gke-gcloud-auth-plugin
+ - export USE_GKE_GCLOUD_AUTH_PLUGIN=True
+ - gcloud container clusters get-credentials applica
+ - kubectl set image deployment/${MODULE} ${MODULE}=${IMAGE} -n ${NAMESPACE}
+ artifacts:
+ - build/**
+pipelines:
+ branches:
+ main:
+ - step: *publish
+ - step: *deploy
\ No newline at end of file
--- /dev/null
+import json
+
+from flask import Flask, jsonify, request
+from predictor import Predictor, PredictionItemEncoder
+
+app = Flask(__name__)
+predictor = Predictor('data/model.h5')
+
+
+@app.route('/predict', methods=['POST'])
+def post_data():
+ json_request = request.get_json()
+ machine = json_request.get('machine')
+ day = json_request.get('day')
+
+ max_hours = json_request.get('max_hours', 24)
+ max_days = json_request.get('max_days', 7)
+
+ threshold = json_request.get('threshold', 0.5)
+
+ breakpoint_hours = json_request.get('breakpoint_hours', 40)
+ accumulated_hours = json_request.get('accumulated_hours', 0)
+
+ if machine is None or day is None:
+ return jsonify({'message': 'Machine and day are required'}), 400
+
+ if not isinstance(max_hours, int) or not isinstance(max_days, int):
+ return jsonify({'message': 'max_hours and max_days must be integers'}), 400
+
+ if max_hours <= 0 or max_days <= 0:
+ return jsonify({'message': 'max_hours and max_days must be positive'}), 400
+
+ predictions = predictor.predict_until_break(machine,
+ day,
+ max_hours,
+ max_days,
+ accumulated_hours,
+ breakpoint_hours,
+ threshold)
+ return jsonify(json.loads(json.dumps(predictions, cls=PredictionItemEncoder))), 200
+
+
+@app.route('/')
+def hello_world():
+ return jsonify({'message': 'Hello, World!'})
+
+
+if __name__ == '__main__':
+ app.run(debug=True, host='0.0.0.0')
--- /dev/null
+import json
+import os
+# Disable Tensorflow warnings
+os.environ['TF_CPP_MIN_LOG_LEVEL'] = '4'
+
+import string
+import tensorflow as tf
+from typing import Hashable
+import matplotlib.pyplot as plt
+
+
+class PredictionItem:
+ """
+ A prediction item.
+ """
+ day = 0
+ hours = 0
+ accumulated_hours = 0
+ broken = False
+
+ def __init__(self, day: int, hours: int, accumulated_hours: int, broken: bool):
+ """
+ Initialize the prediction item.
+
+ :type day: int
+ :param day: The day of the year.
+ :type hours: int
+ :param hours: The number of hours.
+ :type accumulated_hours: int
+ :param accumulated_hours: The accumulated hours.
+ :type broken: bool
+ :param broken: Whether the machine has a break.
+ :rtype: None
+ """
+ self.day = day
+ self.hours = hours
+ self.accumulated_hours = accumulated_hours
+ self.broken = broken
+
+ def __str__(self):
+ """
+ Convert the prediction item to a string.
+
+ :rtype: string
+ :return: The string representation of the prediction item.
+ """
+ return 'Day: %d, Hours: %d, Accumulated Hours: %d, Has Break: %s' % (
+ self.day,
+ self.hours,
+ self.accumulated_hours,
+ self.broken
+ )
+
+
+class PredictionItemEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, PredictionItem):
+ return {
+ "day": obj.day,
+ "hours": obj.hours,
+ "accumulated_hours": obj.accumulated_hours,
+ "broken": obj.broken
+ }
+ return json.JSONEncoder.default(self, obj)
+
+
+class Predictor:
+ """
+ A predictor for machine usage hours and break prediction.
+ """
+ model = None
+
+ def __init__(self, model_path: string):
+ """
+ Initialize the predictor.
+
+ :type model_path: string
+ :param model_path: The path to the model file.
+ :rtype: None
+
+ :exception: Exception if the model file does not exist.
+ """
+ if not os.path.exists(model_path):
+ raise Exception('Model not found: %s' % model_path)
+
+ self.model = tf.keras.models.load_model(model_path)
+
+ @staticmethod
+ def train(train_features: Hashable,
+ train_labels: Hashable,
+ test_features: Hashable,
+ test_labels: Hashable,
+ path: string) -> list:
+ """
+ Train the model.
+
+ :param train_features: The training features.
+ :type train_features: Hashable
+ :param train_labels: The training labels.
+ :type train_labels: Hashable
+ :param test_features: The test features.
+ :type test_features: Hashable
+ :param test_labels: The test labels.
+ :param path: The path to save the model.
+ :return: The test loss and accuracy.
+ """
+ samples = len(train_features)
+ model = tf.keras.Sequential([
+ tf.keras.layers.Embedding(samples, 3, input_length=3),
+ tf.keras.layers.LSTM(2),
+ tf.keras.layers.Dense(1, activation='sigmoid')
+ ])
+
+ model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
+ model.fit(train_features, train_labels, epochs=50, batch_size=20)
+ model.save(path)
+
+ test_loss, test_acc = model.evaluate(test_features, test_labels, verbose=2)
+ return [test_loss, test_acc]
+
+ def predict_hours(self, machine: int, day: int, max_hours: int, threshold: float) -> int:
+ """
+ Predict how many hours the machine will be used on a given day.
+ :param machine: The machine type.
+ :param day: The day of the year.
+ :param max_hours: The maximum number of hours to predict.
+ :param threshold: The threshold to stop predicting.
+ :return: The number of hours the machine will be used.
+ """
+ hours = 1
+ while hours < max_hours:
+ prediction = self.model.predict([[machine, day, hours]])
+ if prediction.item() > threshold:
+ break
+ hours += 1
+
+ return hours
+
+ def predict(self,
+ machine: int,
+ day: int,
+ max_days: int,
+ max_hours: int,
+ accumulated_hours: int,
+ breakpoint_hours: int,
+ threshold: float) -> list[PredictionItem]:
+ """
+ Predict how many hours the machine will be used on a given day.
+ :param machine: The machine type.
+ :param day: The day of the year.
+ :param max_days: The maximum number of days to predict.
+ :param max_hours: The maximum number of hours to predict.
+ :param accumulated_hours: The accumulated hours.
+ :param breakpoint_hours: The breakpoint hours.
+ :param threshold: The threshold to use to validate the prediction.
+ :return: The number of hours the machine will be used.
+ """
+ predictions = []
+ while day < max_days:
+ hours = self.predict_hours(machine, day, max_hours, threshold)
+ day += 1
+ accumulated_hours += hours
+ broken = accumulated_hours >= breakpoint_hours
+ prediction_item = PredictionItem(day, hours, accumulated_hours, broken)
+ predictions.append(prediction_item)
+
+ return predictions
+
+ def predict_until_break(self,
+ machine: int,
+ day: int,
+ max_hours: int,
+ max_days: int,
+ accumulated_hours: int,
+ breakpoint_hours: int,
+ threshold: float) -> list[PredictionItem]:
+ """
+ Predict machine usage hours until a break is predicted.
+ :param machine: The machine type.
+ :param day: The day of the year.
+ :param max_hours: The maximum number of hours to predict.
+ :param max_days: The maximum number of days to predict.
+ :param accumulated_hours: The accumulated hours.
+ :param breakpoint_hours: The breakpoint hours.
+ :param threshold: The threshold to use to validate the prediction.
+ :return: The number of hours the machine will be used.
+ """
+ predictions = []
+ max_days_to_predict = day + max_days
+ while day < max_days_to_predict:
+ hours = self.predict_hours(machine, day, max_hours, threshold)
+ day += 1
+ accumulated_hours += hours
+ broken = accumulated_hours >= breakpoint_hours
+ prediction_item = PredictionItem(day, hours, accumulated_hours, broken)
+ predictions.append(prediction_item)
+
+ if broken:
+ break
+
+ return predictions
+
+ @staticmethod
+ def plot(predictions: list[PredictionItem], threshold: float, machine: int, path: string) -> None:
+ plt.clf()
+ # For each prediction show accumulated hours per day
+ plt.plot([prediction.day for prediction in predictions], [prediction.accumulated_hours for prediction in predictions])
+ # Show red circle when broken is True
+ plt.plot([prediction.day for prediction in predictions if prediction.broken],
+ [prediction.accumulated_hours for prediction in predictions if prediction.broken], 'ro')
+ # Plot hours per day
+ plt.plot([prediction.day for prediction in predictions], [prediction.hours for prediction in predictions])
+ plt.xlabel('Day')
+ plt.ylabel('Hours')
+ plt.legend(['Total Hours', 'Breakpoint', 'Daily Hours'])
+ plt.title('Machine %d' % machine)
+ plt.savefig(path)
--- /dev/null
+pandas~=2.1.4
+scikit-learn
+matplotlib~=3.8.2
+flask~=3.0.1
+numpy~=1.26.3
+tensorflow
\ No newline at end of file
--- /dev/null
+FROM tensorflow/tensorflow
+
+WORKDIR /app
+COPY . /app
+
+# Install pip and then install the requirements
+RUN pip install --upgrade pip
+RUN pip install -r requirements.txt --ignore-installed
+# Configure firewall to allow traffic on port 5000
+
+EXPOSE 5000
+CMD ["python", "main.py"]
\ No newline at end of file
--- /dev/null
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ml
+ labels:
+ app: ml
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: ml
+ template:
+ metadata:
+ labels:
+ app: ml
+ spec:
+ containers:
+ - image: eu.gcr.io/applica-general/edera-ml:latest
+ name: ml
+ ports:
+ - containerPort: 5000
+ name: ml
+ imagePullPolicy: Always
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: ml
+ name: ml
+spec:
+ type: NodePort
+ ports:
+ - port: 5000
+ targetPort: 5000
+ protocol: TCP
+ selector:
+ app: ml
--- /dev/null
+ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiYXBwbGljYS1n
+ZW5lcmFsIiwKICAicHJpdmF0ZV9rZXlfaWQiOiAiNDk3YzZmNTU5N2Y0NTI3NWY3ZTlmZTQwMzFj
+NGI4MjllMjY4NzEyMSIsCiAgInByaXZhdGVfa2V5IjogIi0tLS0tQkVHSU4gUFJJVkFURSBLRVkt
+LS0tLVxuTUlJRXZnSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2d3Z2dTa0FnRUFBb0lCQVFD
+dDRrY1B3UVJ6bUQ3b1xucHlVQVBlTnBnS01TWGxTM25CYmVtSUFlcmk3dm5GeHJIMFV4RlJndmRN
+VTlzRTJ6L0h5WXNTMGNnR1R3Y01Fd1xuQlVVYVF3UWpUMFVxdlBrN3ByY2lOWURSTXVNQXY1MW5Y
+WkpRZ0RYeitMSG15VldhaE1VRTlMczM4YzJxeUFpalxuVTBjakhqTmxsMmp3S2J5OWJVNUVUcWlT
+eEtxdE1FRVlwSUFzV2kvWG1iekx6NlVtSnladm56ZCtkRFNRQTU3aFxuY0NsOEFTdlBpQTdIeXVz
+b1lzVWJ2S2xRS0tmcndtV3NBUllEVjdRc0t6bEpsR0ViWE1sWlozdzB1VCszVkxXOVxuTWMyS1Fo
+SGpvaWpoTW9yRjdVdjVWMjJrTWkyL2V4RkI3L3ErbllCejU5K1MxUXZlRmpCd3E5L0gwQTRyTXpU
+a1xubWZDYzhjMWZBZ01CQUFFQ2dnRUFOR1dsdlRrUUl0Y2pTYzhvSnJEL2lLaXpPeE5DMnd0Rmx2
+RUVWbnB0ZVZXNFxuUWExc0Y3VEFFM2pRQU4xU0pPVDJGTHI3R1lZVkpLRU5qZTlnbWQvTTdPanpz
+a084cEwyQm5PVGJldTZuR2ZBalxudWVTbjlPc1ZsdjEvZWtoOEs3Skxma2xTNnpKSm8rZGdOdnNl
+eWhYTkxoVllrVm82WGlpRWQ2L3VPei9aSUpPVVxuTFlHN1hPVHExNWhKcmZaM2trdDBBRldJc2NK
+SXVFbFBlUTJOaDVWY3VSeVJwUVQwaFA0NUlRUTZFdElBWXp4OVxuUnRGUWFhd2tpWVFqZ0RkelFO
+Qm1tUHN2ZWc4Y2JkQXlWZVlxcitaVkFiNk1pWjJ2N01RR0hyeEFBcnhSeXRMZlxuMmd2VUl0K3pv
+dmJBemo3Q3h0YjUvS3JDdHUwMC9aWTcwNzRxTExnR1NRS0JnUUR2NXRkNmxWbUg5cXl3TThTc1xu
+NnA5NE5lTVFrbU9vME41SjBUUisxTGFITnBkdjBaY3B0bEJDS0lCTXorVTBBc0p0cG11dlpGTGdH
+cmp6U3E2cFxuVFlGUis0Mlc1NjJGMkoxWmlKZmdnWDdubWp3TmhUNzdrdWUwbjVWbE1iTHBUN0Iz
+ajB6V3Ftc0hqZ09lZWhDblxueEIwOXpKcXJEYm96MkV3UGVvRkhDVjB5dHdLQmdRQzVqVmpJd21F
+d01PU2g5UHF3QzJ5SVBYeS81UkpGTG4yL1xuNWxzUmhRbkkrTkxTZUJjWnRIV1BiOWcyNVF6SFVV
+dWRRNGhiYWpHaDM1R2t5SGtIcjJ5MGVjdTFUd2pVTitGMFxubzdhejRzekk4a3kzRk9sRUpvdXM4
+enFNQWsrYnhFR1hrUUNkWmFiL0ltS0svR1dRK21RcXlDV0RtUkFvR3NVcVxuTEJNdUhKOXltUUtC
+Z0YrRnJBRGNYT1RkWEk5Z1hZeDRjM3piQUFtR01IWjBqRDRhTmV2V2FNTllBbDU4dHRMZVxuREFE
+N3ZYSllTU3czZVJGTjlZekZ4cFlETGVkNXNpZ3BlemVZa1Iwb0xKaWgwcTFtelFxUXBXWTBySHE1
+dG9WWFxuVGpsR1hhY0liZk9tVGw2Y3lYeWtLSysrWlVTQjJBWGsrYnUwcjFVeXh4U0RxRzExV3Vw
+ZEdTWHJBb0dCQUo2YVxucm9CMGZvU2wxbGlGd2Q3RzlRK0RsMldqMWJraTQwUXNFRDNxZlJHM2R1
+V0cxeUFXdThKT3RQOC9UR3YzRm00blxuc3ArSkowR1ppN0hSMW5wMlBiSUt4ZENGN1NNUlhQckpr
+YnN6cXg0ODFzeEw2SlJqYWxMOFdWZ2lCWkE4OG1BdlxuQnRxRGNIcDNGc3A4c2doNXJ6Tk9mNXA4
+Tkc1RGE3TC9sNmw3dCtOSkFvR0JBT2d6MHM1WGErRzZLTmlVN0IySFxuc0l2MmRNMzZvNlZ3a21P
+T3FjdCtkVS8ycG12ZGM4aFpDbFZGYXBQeWZ4djFqVnNjSDUrZG5FaFJIaTEzYXdIU1xuVzc3MnQ5
+WUdBNWpGb2dnakVPRU5QbVR2QUwxd01NL0ExRCtmanVaWVArOEVDaEU0QmFmbVN3WWZGVzJRQmxE
+eFxuZlhoOHNmT0FnMjhuMEc1QWNGcXZVSFJNXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4i
+LAogICJjbGllbnRfZW1haWwiOiAiYml0YnVja2V0LXBpcGVsaW5lc0BhcHBsaWNhLWdlbmVyYWwu
+aWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTA4Mzk0ODE1Njc3NDgx
+NDI2MjYwIiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1
+dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20v
+dG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29v
+Z2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAi
+aHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9iaXRidWNr
+ZXQtcGlwZWxpbmVzJTQwYXBwbGljYS1nZW5lcmFsLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwK
+ICAidW5pdmVyc2VfZG9tYWluIjogImdvb2dsZWFwaXMuY29tIgp9Cg==
\ No newline at end of file
--- /dev/null
+export PROJECT_ID=applica-general
+export PROJECT_NAME=applica-general
+export NAMESPACE=edera
+export MODULE=ml
\ No newline at end of file
--- /dev/null
+import pandas as pd
+import numpy as np
+import string
+
+
+class Simulator:
+ @staticmethod
+ def __generate_data(path: string, machines: int, size: int) -> None:
+ df = pd.DataFrame(columns=['machine', 'date', 'hours', 'failure'])
+ df['machine'] = np.random.randint(1, machines, size=size)
+ df['date'] = np.random.randint(1, 365, size=size)
+ df['hours'] = np.random.randint(1, 8, size=size)
+ df['failure'] = np.random.randint(0, 2, size=size)
+ df.to_csv(path, index=False)
+
+ @staticmethod
+ def __load_data(path: string) -> pd.DataFrame:
+ df = pd.read_csv(path)
+ return df.sample(frac=1).reset_index(drop=True)
+
+ @staticmethod
+ def __prepare(df: pd.DataFrame) -> pd.DataFrame:
+ features = df[['machine', 'date', 'hours']]
+ labels = df[['failure']]
+
+ return features, labels
+
+ def generate(self, machines: int, size: int, path: string) -> pd.DataFrame:
+ self.__generate_data(path, machines, size)
+ data = self.__load_data(path)
+ return self.__prepare(data)
--- /dev/null
+from simulator import Simulator
+from predictor import Predictor
+
+# Configuration
+
+# machines = 10 # Number of machine types to simulate
+# samples = 5000 # Number of samples to generate
+
+# simulator = Simulator()
+# train_features, train_labels = simulator.generate(machines, samples, 'data/train.csv')
+# test_features, test_labels = simulator.generate(machines, samples, 'data/test.csv')
+
+predictor = Predictor('data/model.h5')
+
+# predictions = predictor.predict(1, 1, 30, 8, 9, 120, 0.51)
+predictions = predictor.predict_until_break(1, 1, 8, 0, 120, 0.51)
+for prediction in predictions:
+ print(prediction)
+
+
+predictor.plot(predictions, 0.7, 0, 'data/plot.png')