From 0875e17ce1937d5e1289cb9e45ad65f4ca1faff8 Mon Sep 17 00:00:00 2001 From: Roberto Stomeo Date: Tue, 1 Jul 2025 16:48:29 +0200 Subject: [PATCH] Initial commit edera-ml --- edera-ml/.dockerignore | 3 + edera-ml/.gitignore | 6 + edera-ml/README.md | 39 ++++ edera-ml/bitbucket-pipelines.yml | 54 +++++ edera-ml/data/model.h5 | Bin 0 -> 71288 bytes edera-ml/data/plot.png | Bin 0 -> 27584 bytes edera-ml/main.py | 49 +++++ edera-ml/predictor.py | 217 +++++++++++++++++++ edera-ml/requirements.txt | 6 + edera-ml/scripts/docker/Dockerfile | 12 + edera-ml/scripts/kube/api.yml | 38 ++++ edera-ml/scripts/service-account.json.base64 | 42 ++++ edera-ml/scripts/set-env.sh | 4 + edera-ml/simulator.py | 31 +++ edera-ml/test.py | 21 ++ 15 files changed, 522 insertions(+) create mode 100644 edera-ml/.dockerignore create mode 100644 edera-ml/.gitignore create mode 100644 edera-ml/README.md create mode 100644 edera-ml/bitbucket-pipelines.yml create mode 100644 edera-ml/data/model.h5 create mode 100644 edera-ml/data/plot.png create mode 100644 edera-ml/main.py create mode 100644 edera-ml/predictor.py create mode 100644 edera-ml/requirements.txt create mode 100644 edera-ml/scripts/docker/Dockerfile create mode 100644 edera-ml/scripts/kube/api.yml create mode 100644 edera-ml/scripts/service-account.json.base64 create mode 100644 edera-ml/scripts/set-env.sh create mode 100644 edera-ml/simulator.py create mode 100644 edera-ml/test.py diff --git a/edera-ml/.dockerignore b/edera-ml/.dockerignore new file mode 100644 index 0000000..02c3065 --- /dev/null +++ b/edera-ml/.dockerignore @@ -0,0 +1,3 @@ +env/* +data/*.csv +data/*.png \ No newline at end of file diff --git a/edera-ml/.gitignore b/edera-ml/.gitignore new file mode 100644 index 0000000..d2c67ab --- /dev/null +++ b/edera-ml/.gitignore @@ -0,0 +1,6 @@ +*.csv +__pycache__ +*.pyc +*.pyo +env/ +.idea/ \ No newline at end of file diff --git a/edera-ml/README.md b/edera-ml/README.md new file mode 100644 index 0000000..02b211e --- /dev/null +++ b/edera-ml/README.md @@ -0,0 +1,39 @@ +# 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") +``` diff --git a/edera-ml/bitbucket-pipelines.yml b/edera-ml/bitbucket-pipelines.yml new file mode 100644 index 0000000..edafbc8 --- /dev/null +++ b/edera-ml/bitbucket-pipelines.yml @@ -0,0 +1,54 @@ +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 diff --git a/edera-ml/data/model.h5 b/edera-ml/data/model.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c74ca73aa54f0b79e591d1f80872b5dfcccc9db7 GIT binary patch literal 71288 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 Gc>E6uOMz(s literal 0 HcmV?d00001 diff --git a/edera-ml/data/plot.png b/edera-ml/data/plot.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7fd63b3ffeba2fd39feb83fa8b5f8a99fc9fa0 GIT binary patch literal 27584 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 literal 0 HcmV?d00001 diff --git a/edera-ml/main.py b/edera-ml/main.py new file mode 100644 index 0000000..e01128c --- /dev/null +++ b/edera-ml/main.py @@ -0,0 +1,49 @@ +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') diff --git a/edera-ml/predictor.py b/edera-ml/predictor.py new file mode 100644 index 0000000..ed4d098 --- /dev/null +++ b/edera-ml/predictor.py @@ -0,0 +1,217 @@ +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) diff --git a/edera-ml/requirements.txt b/edera-ml/requirements.txt new file mode 100644 index 0000000..c659d70 --- /dev/null +++ b/edera-ml/requirements.txt @@ -0,0 +1,6 @@ +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 diff --git a/edera-ml/scripts/docker/Dockerfile b/edera-ml/scripts/docker/Dockerfile new file mode 100644 index 0000000..66170c6 --- /dev/null +++ b/edera-ml/scripts/docker/Dockerfile @@ -0,0 +1,12 @@ +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 diff --git a/edera-ml/scripts/kube/api.yml b/edera-ml/scripts/kube/api.yml new file mode 100644 index 0000000..e67dae4 --- /dev/null +++ b/edera-ml/scripts/kube/api.yml @@ -0,0 +1,38 @@ +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 diff --git a/edera-ml/scripts/service-account.json.base64 b/edera-ml/scripts/service-account.json.base64 new file mode 100644 index 0000000..6841642 --- /dev/null +++ b/edera-ml/scripts/service-account.json.base64 @@ -0,0 +1,42 @@ +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 diff --git a/edera-ml/scripts/set-env.sh b/edera-ml/scripts/set-env.sh new file mode 100644 index 0000000..5443338 --- /dev/null +++ b/edera-ml/scripts/set-env.sh @@ -0,0 +1,4 @@ +export PROJECT_ID=applica-general +export PROJECT_NAME=applica-general +export NAMESPACE=edera +export MODULE=ml \ No newline at end of file diff --git a/edera-ml/simulator.py b/edera-ml/simulator.py new file mode 100644 index 0000000..d79a461 --- /dev/null +++ b/edera-ml/simulator.py @@ -0,0 +1,31 @@ +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) diff --git a/edera-ml/test.py b/edera-ml/test.py new file mode 100644 index 0000000..f090dca --- /dev/null +++ b/edera-ml/test.py @@ -0,0 +1,21 @@ +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') -- 2.34.1