Skip to content

Commit

Permalink
Merge pull request #5 from linomp/feat/simulate-maintenance
Browse files Browse the repository at this point in the history
implement maintenance & fine tune some weights and constants
  • Loading branch information
linomp authored Jan 21, 2024
2 parents 522c8c8 + 3b9a875 commit c0968aa
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 49 deletions.
3 changes: 2 additions & 1 deletion mvp/client/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export type { OpenAPIConfig } from './core/OpenAPI';

export type { GameSessionDTO } from './models/GameSessionDTO';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { MachineStats } from './models/MachineStats';
export type { MachineState } from './models/MachineState';
export type { OperationalParameters } from './models/OperationalParameters';
export type { ValidationError } from './models/ValidationError';

export { DefaultService } from './services/DefaultService';
export { MachineInterventionsService } from './services/MachineInterventionsService';
export { SessionsService } from './services/SessionsService';
4 changes: 2 additions & 2 deletions mvp/client/src/generated/models/GameSessionDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
/* tslint:disable */
/* eslint-disable */

import type { MachineStats } from './MachineStats';
import type { MachineState } from './MachineState';

export type GameSessionDTO = {
id: string;
current_step: number;
machine_stats?: (MachineStats | null);
machine_state?: (MachineState | null);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import type { OperationalParameters } from './OperationalParameters';

export type MachineStats = {
export type MachineState = {
predicted_rul?: (number | null);
health_percentage: number;
operational_parameters?: (OperationalParameters | null);
operational_parameters: OperationalParameters;
};
34 changes: 34 additions & 0 deletions mvp/client/src/generated/services/MachineInterventionsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GameSessionDTO } from '../models/GameSessionDTO';

import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';

export class MachineInterventionsService {

/**
* Do Maintenance
* @param sessionId
* @returns GameSessionDTO Successful Response
* @throws ApiError
*/
public static doMaintenanceSessionMachineInterventionsMaintenancePost(
sessionId: string,
): CancelablePromise<GameSessionDTO> {
return __request(OpenAPI, {
method: 'POST',
url: '/session/machine/interventions/maintenance',
query: {
'session_id': sessionId,
},
errors: {
422: `Validation Error`,
},
});
}

}
32 changes: 30 additions & 2 deletions mvp/client/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<script lang="ts">
import { SessionsService, type GameSessionDTO, OpenAPI } from '../generated';
import {
SessionsService,
type GameSessionDTO,
OpenAPI,
MachineInterventionsService
} from '../generated';
import runningMachineSrc from '$lib/assets/healthy.gif';
const stoppedMachineSrc = new URL('../lib/assets/stopped.PNG', import.meta.url).href;
let gameSession: GameSessionDTO | null;
let gameOver = false;
let advanceButtonDisabled = false;
let maintenanceButtonDisabled = false;
let stopAnimation = false;
Expand Down Expand Up @@ -47,6 +53,7 @@
// start fetching machine health every second while the day is advancing
const intervalId = setInterval(fetchExistingSession, 500);
advanceButtonDisabled = true;
maintenanceButtonDisabled = true;
try {
gameSession = await SessionsService.advanceSessionTurnsPut(gameSession?.id);
Expand All @@ -57,6 +64,24 @@
// stop fetching machine health until the player advances to next day again
clearInterval(intervalId);
advanceButtonDisabled = false;
maintenanceButtonDisabled = false;
}
};
const doMaintenance = async () => {
if (!gameSession || gameOver) {
return;
}
try {
maintenanceButtonDisabled = true;
gameSession =
await MachineInterventionsService.doMaintenanceSessionMachineInterventionsMaintenancePost(
gameSession?.id
);
} catch (error) {
console.error('Error performing maintenance:', error);
maintenanceButtonDisabled = false;
}
};
Expand All @@ -65,7 +90,7 @@
return;
}
if (gameSession?.machine_stats && gameSession.machine_stats.health_percentage <= 0) {
if (gameSession?.machine_state && gameSession.machine_state.health_percentage <= 0) {
gameOver = true;
}
};
Expand Down Expand Up @@ -94,6 +119,9 @@
<button on:click={advanceToNextDay} disabled={advanceButtonDisabled}
>Advance to next day</button
>
<button on:click={doMaintenance} disabled={maintenanceButtonDisabled}
>Perform Maintenance</button
>
{/if}
{:else}
<button on:click={startSession}>Start Session</button>
Expand Down
8 changes: 5 additions & 3 deletions mvp/server/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

def compute_decay_speed(parameter_values: 'OperationalParameters') -> float:
# TODO: calibrate these weights
temperature_weight = 0.001
temperature_weight = 0.0005
oil_age_weight = 0.001
mechanical_wear_weight = 0.01
mechanical_wear_weight = 0.005

# Made-up calculation involving operational parameters: temperature, oil age, mechanical wear
computed = parameter_values.temperature * temperature_weight + \
Expand All @@ -18,7 +18,9 @@ def compute_decay_speed(parameter_values: 'OperationalParameters') -> float:
OIL_AGE_MAPPING_MAX * oil_age_weight + \
MECHANICAL_WEAR_MAPPING_MAX * mechanical_wear_weight

return map_value(computed, from_low=0, from_high=mapping_max, to_low=0, to_high=0.3)
computed = min(mapping_max, computed)

return map_value(computed, from_low=0, from_high=mapping_max, to_low=0, to_high=0.1)


def default_rul_prediction_fn(current_timestep: int) -> int | None:
Expand Down
14 changes: 13 additions & 1 deletion mvp/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse, JSONResponse

from mvp.server.data_models.GameSession import GameSession, GameSessionDTO
from mvp.server.core.GameSession import GameSession, GameSessionDTO

app = FastAPI()
app.add_middleware(
Expand Down Expand Up @@ -59,3 +59,15 @@ async def advance(session_id: str) -> GameSessionDTO | JSONResponse:
await session.advance_one_turn()

return GameSessionDTO.from_session(session)


@app.post("/session/machine/interventions/maintenance", response_model=GameSessionDTO, tags=["Machine Interventions"])
async def do_maintenance(session_id: str) -> GameSessionDTO | JSONResponse:
if session_id not in sessions:
return JSONResponse(status_code=404, content={"message": "Session not found"})

session = sessions[session_id]

session.do_maintenance()

return GameSessionDTO.from_session(session)
2 changes: 1 addition & 1 deletion mvp/server/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
TEMPERATURE_STARTING_POINT = 20
OIL_AGE_MAPPING_MAX = 365
TEMPERATURE_MAPPING_MAX = 200
MECHANICAL_WEAR_MAPPING_MAX = 1000
MECHANICAL_WEAR_MAPPING_MAX = 100
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

from mvp.server.analysis import default_rul_prediction_fn
from mvp.server.constants import TIMESTEPS_PER_MOVE
from mvp.server.data_models.MachineState import MachineState
from mvp.server.core.MachineState import MachineState


class GameSession(BaseModel):
id: str
current_step: int = 0
machine_stats: MachineState
machine_state: MachineState

# TODO: Update this function in-game, to simulate a change in the model (an "upgrade" for the player)
rul_predictor: Callable[[int], int | None] = default_rul_prediction_fn
Expand All @@ -21,54 +21,62 @@ def new_game_session(_id: str):
return GameSession(
id=_id,
current_step=0,
machine_stats=MachineState.new_machine_stats()
machine_state=MachineState.new_machine_state()
)

def is_game_over(self) -> bool:
if self.machine_state.is_broken():
print(f"GameSession '{self.id}' - machine failed at step {self.current_step} - {self.machine_state}")
return True
# TODO: add other game over conditions, i.e. player ran out of money
return False

async def advance_one_turn(self) -> list[MachineState]:
collected_machine_stats_during_turn = []
collected_machine_state_during_turn = []

for _ in range(TIMESTEPS_PER_MOVE):
# collect stats
collected_machine_stats_during_turn.append(self.machine_stats)
collected_machine_state_during_turn.append(self.machine_state)

# check if game over
if self.machine_stats.is_broken():
print(f"GameSession '{self.id}' - machine failed at step {self.current_step} - {self.machine_stats}")
if self.is_game_over():
break

self.current_step += 1
self.machine_stats.update_stats_and_parameters(self.current_step, self.rul_predictor)
self.machine_state.update_stats_and_parameters(self.current_step, self.rul_predictor)
self._log()

await asyncio.sleep(0.5)

return collected_machine_stats_during_turn
return collected_machine_state_during_turn

def do_maintenance(self):
self.machine_stats.simulate_maintenance()
# TODO - add a cost for maintenance, reject if player doesn't have enough money
self.machine_state.simulate_maintenance()

def _log(self, multiple=5):
if self.current_step % multiple == 0:
print(f"GameSession '{self.id}' - step: {self.current_step} - {self.machine_stats}")
print(f"GameSession '{self.id}' - step: {self.current_step} - {self.machine_state}")


class GameSessionDTO(BaseModel):
id: str
current_step: int
machine_stats: MachineState | None = None
machine_state: MachineState | None = None

# TODO: define MachineStateDTO to hide some fields from the player, e.g. health_percentage

@staticmethod
def from_session(session: 'GameSession'):
return GameSessionDTO(
id=session.id,
current_step=session.current_step,
machine_stats=session.machine_stats,
machine_state=session.machine_state,
)

@staticmethod
def from_dict(json: dict[str, Any]):
return GameSessionDTO(
id=json.get("id", ""),
current_step=json.get("current_step", 0),
machine_stats=MachineState.from_dict(json.get("machine_stats", {}))
machine_state=MachineState.from_dict(json.get("machine_state", {}))
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from mvp.server.analysis import compute_decay_speed
from mvp.server.constants import TEMPERATURE_STARTING_POINT
from mvp.server.data_models.OperationalParameters import OperationalParameters
from mvp.server.core.OperationalParameters import OperationalParameters
from mvp.server.math_utils import exponential_decay


Expand All @@ -14,7 +14,7 @@ class MachineState(BaseModel):
operational_parameters: OperationalParameters

@staticmethod
def new_machine_stats():
def new_machine_state():
return MachineState(
health_percentage=100,
operational_parameters=OperationalParameters(
Expand All @@ -31,11 +31,11 @@ def simulate_maintenance(self):
self.operational_parameters = OperationalParameters(
temperature=TEMPERATURE_STARTING_POINT,
oil_age=0,
mechanical_wear=self.operational_parameters.mechanical_wear / 2
mechanical_wear=self.operational_parameters.mechanical_wear / 10
)
# TODO: confirm if it makes sense to reset the health percentage; maybe should be kept as is,
# TODO: decide if it makes sense to restore some health percentage; maybe should be kept as is,
# maintenance does not mean "new" machine, only slows down the decay
# self.health_percentage = 100
self.health_percentage = min(100, max(0, round(self.health_percentage * 1.05)))

def update_stats_and_parameters(self, timestep: int, rul_predictor: Callable[[int], int | None] = None):
self.operational_parameters.update(timestep)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,23 @@ def compute_machine_temperature(self, current_timestep: int) -> float:

def compute_oil_age(self, current_timestep: int) -> float:
# oil age grows monotonically and resets only after every maintenance routine
raw_value = self.oil_age + (current_timestep * self.temperature)
raw_value = min(1e6, self.oil_age + (current_timestep * self.temperature))
return map_value(
raw_value,
from_low=self.oil_age,
from_high=1e5,
from_high=1e6,
to_low=self.oil_age,
to_high=OIL_AGE_MAPPING_MAX
)

def compute_mechanical_wear(self, current_timestep: int) -> float:
# mechanical wear grows monotonically, directly proportional to oil ag.
# for now it never resets (such that at some point, the machine will definitely break and game over)
raw_value = math.exp(current_timestep) * self.oil_age / 1e6
raw_value = min(1e6, math.exp(current_timestep) * self.oil_age / 1e16)
return map_value(
raw_value,
from_low=0,
from_high=1e12,
from_high=1e6,
to_low=self.mechanical_wear,
to_high=MECHANICAL_WEAR_MAPPING_MAX
)
Expand Down
File renamed without changes.
Loading

0 comments on commit c0968aa

Please sign in to comment.