/* * JaamSim Discrete Event Simulation * Copyright (C) 2014 Ausenco Engineering Canada Inc. * Copyright (C) 2016 JaamSim Software Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.BasicObjects; import java.util.ArrayList; import com.jaamsim.Samples.SampleInput; import com.jaamsim.Samples.SampleProvider; import com.jaamsim.basicsim.Entity; import com.jaamsim.basicsim.EntityTarget; import com.jaamsim.basicsim.Simulation; import com.jaamsim.events.EventHandle; import com.jaamsim.events.EventManager; import com.jaamsim.events.ProcessTarget; import com.jaamsim.input.BooleanInput; import com.jaamsim.input.EntityInput; import com.jaamsim.input.EnumInput; import com.jaamsim.input.InputErrorException; import com.jaamsim.input.Keyword; import com.jaamsim.input.Output; import com.jaamsim.states.DowntimeUser; import com.jaamsim.states.StateEntity; import com.jaamsim.states.StateEntityListener; import com.jaamsim.states.StateRecord; import com.jaamsim.units.TimeUnit; public class DowntimeEntity extends StateEntity implements StateEntityListener { public enum DowntimeTypes { IMMEDIATE, FORCED, OPPORTUNISTIC } @Keyword(description = "The calendar or working time for the first planned or unplanned " + "maintenance event. If an input is not provided, the first maintenance " + "event is determined by the input for the Interval keyword. A number, " + "an object that returns a number, or an expression can be entered.", exampleList = {"720 h", "UniformDistribution1" }) private final SampleInput firstDowntime; @Keyword(description = "The object whose working time determines the occurrence of the " + "planned or unplanned maintenance events. Calendar time is used if " + "the input is left blank.", exampleList = {"Object1"}) private final EntityInput<StateEntity> iatWorkingEntity; @Keyword(description = "The object whose working time determines the completion of the " + "planned or unplanned maintenance activity. Calendar time is used if " + "the input is left blank.", exampleList = {"Object1"}) private final EntityInput<StateEntity> durationWorkingEntity; @Keyword(description = "The calendar or working time between the start of the last planned or " + "unplanned maintenance activity and the start of the next maintenance " + "activity. A number, an expression, or an object that returns a number " + "can be entered.", exampleList = {"168 h", "IntervalValueSequence", "IntervalDistribution" }) private final SampleInput downtimeIATDistribution; @Keyword(description = "The calendar or working time required to complete the planned or " + "unplanned maintenance activity. A number, an expression, or an object " + "that returns a number can be entered.", exampleList = {"8 h ", "DurationValueSequence", "DurationDistribution" }) private final SampleInput downtimeDurationDistribution; @Keyword(description = "The severity level for the downtime events. The input must be one of " + "the following:\n" + "- IMMEDIATE (interrupts the present task and starts maintenance " + "without delay)\n" + "- FORCED (completes the present task before starting maintenance)\n" + "- OPPORTUNISTIC (completes the present task and any waiting tasks " + "before starting maintenance)\n" + "Planned maintenace normally uses the OPPORTUNISTIC setting, while " + "unplanned maintenance (breakdowns) normally uses the IMMEDIATE " + "setting.", exampleList = {"FORCED"}) private final EnumInput<DowntimeTypes> type; @Keyword(description = "If TRUE, the downtime event can occur in parallel with another " + "downtime event.", exampleList = {"FALSE"}) protected final BooleanInput concurrent; private final ArrayList<DowntimeUser> downtimeUserList; // entities that use this downtime entity private boolean down; // true for the duration of a downtime event private int downtimePendings; // number of queued downtime events private double downtimePendingStartTime; // the simulation time in seconds at which the downtime pending started private double secondsForNextFailure; // The number of working seconds required before the next downtime event private double secondsForNextRepair; // The number of working seconds required before the downtime event ends private double startTime; // The start time of the latest downtime event private double endTime; // the end time of the latest downtime event { workingStateListInput.setHidden(true); firstDowntime = new SampleInput("FirstDowntime", "Key Inputs", null); firstDowntime.setUnitType(TimeUnit.class); this.addInput(firstDowntime); iatWorkingEntity = new EntityInput<>(StateEntity.class, "IntervalWorkingEntity", "Key Inputs", null); this.addInput(iatWorkingEntity); this.addSynonym(iatWorkingEntity, "IATWorkingEntity"); durationWorkingEntity = new EntityInput<>(StateEntity.class, "DurationWorkingEntity", "Key Inputs", null); this.addInput(durationWorkingEntity); downtimeIATDistribution = new SampleInput("Interval", "Key Inputs", null); downtimeIATDistribution.setUnitType(TimeUnit.class); downtimeIATDistribution.setEntity(this); downtimeIATDistribution.setRequired(true); this.addInput(downtimeIATDistribution); this.addSynonym(downtimeIATDistribution, "IAT"); this.addSynonym(downtimeIATDistribution, "TimeBetweenFailures"); downtimeDurationDistribution = new SampleInput("Duration", "Key Inputs", null); downtimeDurationDistribution.setUnitType(TimeUnit.class); downtimeDurationDistribution.setEntity(this); downtimeDurationDistribution.setRequired(true); this.addInput(downtimeDurationDistribution); this.addSynonym(downtimeDurationDistribution, "TimeToRepair"); type = new EnumInput<> (DowntimeTypes.class, "Type", "Key Inputs", null); type.setHidden(true); this.addInput(type); concurrent = new BooleanInput("Concurrent", "Key Inputs", false); concurrent.setHidden(true); this.addInput(concurrent); } public DowntimeEntity(){ downtimeUserList = new ArrayList<>(); } @Override public void validate() throws InputErrorException { super.validate(); if (downtimeIATDistribution.getValue().getMinValue() < 0) throw new InputErrorException("Interval values can not be less than 0."); if (downtimeDurationDistribution.getValue().getMinValue() < 0) throw new InputErrorException("Duration values can not be less than 0."); } @Override public void earlyInit(){ super.earlyInit(); down = false; downtimeUserList.clear(); downtimePendings = 0; downtimePendingStartTime = 0.0; startTime = 0; endTime = 0; if (!this.isActive()) return; for (StateEntity each : Entity.getClonesOfIterator(StateEntity.class, DowntimeUser.class)) { if (!each.isActive()) continue; DowntimeUser du = (DowntimeUser)each; if (du.getMaintenanceEntities().contains(this) || du.getBreakdownEntities().contains(this)) downtimeUserList.add(du); } } @Override public void lateInit() { super.lateInit(); // Determine the time for the first downtime event if (firstDowntime.getValue() == null) secondsForNextFailure = getNextDowntimeIAT(); else secondsForNextFailure = firstDowntime.getValue().getNextSample(getSimTime()); } @Override public void startUp() { super.startUp(); if (!this.isActive()) return; checkProcessNetwork(); } public DowntimeTypes getType() { return type.getValue(); } /** * Get the name of the initial state this Entity will be initialized with. * @return */ @Override public String getInitialState() { return "Working"; } /** * Tests the given state name to see if it is valid for this Entity. * @param state * @return */ @Override public boolean isValidState(String state) { return "Working".equals(state) || "Downtime".equals(state); } /** * Tests the given state name to see if it is counted as working hours when in * that state.. * @param state * @return */ @Override public boolean isValidWorkingState(String state) { return "Working".equals(state); } /** * EndDowntimeTarget */ private static class EndDowntimeTarget extends EntityTarget<DowntimeEntity> { EndDowntimeTarget(DowntimeEntity ent) { super(ent, "endDowntime"); } @Override public void process() { ent.endDowntime(); } } private final ProcessTarget endDowntime = new EndDowntimeTarget(this); private final EventHandle endDowntimeHandle = new EventHandle(); /** * ScheduleDowntimeTarget */ private static class ScheduleDowntimeTarget extends EntityTarget<DowntimeEntity> { ScheduleDowntimeTarget(DowntimeEntity ent) { super(ent, "scheduleDowntime"); } @Override public void process() { ent.scheduleDowntime(); } } private final ProcessTarget scheduleDowntime = new ScheduleDowntimeTarget(this); private final EventHandle scheduleDowntimeHandle = new EventHandle(); /** * Monitors the accumulation of time towards the start of the next maintenance activity or * the completion of the present maintenance activity. This method is called whenever an * entity affected by this type of maintenance changes state. */ public void checkProcessNetwork() { // Schedule the next downtime event if (!scheduleDowntimeHandle.isScheduled()) { // 1) Calendar time if (iatWorkingEntity.getValue() == null) { double workingSecs = this.getSimTime(); double waitSecs = secondsForNextFailure - workingSecs; scheduleProcess(Math.max(waitSecs, 0.0), 5, scheduleDowntime, scheduleDowntimeHandle); } // 2) Working time else { if (iatWorkingEntity.getValue().isWorking()) { double workingSecs = iatWorkingEntity.getValue().getWorkingTime(); double waitSecs = secondsForNextFailure - workingSecs; scheduleProcess(Math.max(waitSecs, 0.0), 5, scheduleDowntime, scheduleDowntimeHandle); } } } // the next event is already scheduled. If the working entity has stopped working, need to cancel the event else { if (iatWorkingEntity.getValue() != null && !iatWorkingEntity.getValue().isWorking()) { EventManager.killEvent(scheduleDowntimeHandle); } } // 1) Determine when to end the current downtime event if (down) { if (durationWorkingEntity.getValue() == null) { if (endDowntimeHandle.isScheduled()) return; // Calendar time double workingSecs = this.getSimTime(); double waitSecs = secondsForNextRepair - workingSecs; scheduleProcess(waitSecs, 5, endDowntime, endDowntimeHandle); return; } // The Entity is working, schedule the end of the downtime event if (durationWorkingEntity.getValue().isWorking()) { if (endDowntimeHandle.isScheduled()) return; double workingSecs = durationWorkingEntity.getValue().getWorkingTime(); double waitSecs = secondsForNextRepair - workingSecs; scheduleProcess(waitSecs, 5, endDowntime, endDowntimeHandle); } // The Entity is not working, remove scheduled end of the downtime event else { EventManager.killEvent(endDowntimeHandle); } } // 2) Start the next downtime event if required/possible else { if (downtimePendings > 0) { // If all entities are ready, start the downtime event boolean allEntitiesCanStart = true; for (DowntimeUser each : downtimeUserList) { if (!each.canStartDowntime(this)) { allEntitiesCanStart = false; break; } } if (allEntitiesCanStart) { this.startDowntime(); } } } } // PrepareForDowntimeTarget private static final class PrepareForDowntimeTarget extends ProcessTarget { private final DowntimeEntity ent; private final DowntimeUser user; public PrepareForDowntimeTarget(DowntimeEntity e, DowntimeUser u) { ent = e; user = u; } @Override public void process() { user.prepareForDowntime(ent); } @Override public String getDescription() { return user.getName() + ".prepareForDowntime"; } } public void scheduleDowntime() { downtimePendings++; if( downtimePendings == 1 ) downtimePendingStartTime = this.getSimTime(); // Determine the time the next downtime event is due // Calendar time based if( iatWorkingEntity.getValue() == null ) { secondsForNextFailure += this.getNextDowntimeIAT(); } // Working time based else { secondsForNextFailure = iatWorkingEntity.getValue().getWorkingTime() + this.getNextDowntimeIAT(); } // prepare all entities for the downtime event for (DowntimeUser each : downtimeUserList) { EventManager.startProcess(new PrepareForDowntimeTarget(this, each)); } this.checkProcessNetwork(); } /** * When enough working hours have been accumulated by WorkingEntity, trigger all entities in downtimeUserList to perform downtime */ private void startDowntime() { setDown(true); startTime = this.getSimTime(); downtimePendings--; // Determine the time when the downtime event will be over double downDuration = this.getDowntimeDuration(); // Calendar time based if( durationWorkingEntity.getValue() == null ) { secondsForNextRepair = this.getSimTime() + downDuration; } // Working time based else { secondsForNextRepair = durationWorkingEntity.getValue().getWorkingTime() + downDuration; } endTime = startTime + downDuration; // Loop through all objects that this object is watching and trigger them to stop working. for (DowntimeUser each : downtimeUserList) { each.startDowntime(this); } this.checkProcessNetwork(); } private void setDown(boolean b) { down = b; if (down) setPresentState("Downtime"); else setPresentState("Working"); } final void endDowntime() { setDown(false); // Loop through all objects that this object is watching and try to restart them. for (DowntimeUser each : downtimeUserList) { each.endDowntime(this); } this.checkProcessNetwork(); } /** * Return the time in seconds of the next downtime IAT */ private double getNextDowntimeIAT() { return downtimeIATDistribution.getValue().getNextSample(getSimTime()); } /** * Return the expected time in seconds of the first downtime */ public double getExpectedFirstDowntime() { return firstDowntime.getValue().getMeanValue( getSimTime() ); } /** * Return the expected time in seconds of the downtime IAT */ public double getExpectedDowntimeIAT() { return downtimeIATDistribution.getValue().getMeanValue( getSimTime() ); } /** * Return the expected time in seconds of the downtime duration */ public double getExpectedDowntimeDuration() { return downtimeDurationDistribution.getValue().getMeanValue( getSimTime() ); } /** * Return the time in seconds of the next downtime duration */ private double getDowntimeDuration() { return downtimeDurationDistribution.getValue().getNextSample(getSimTime()); } public SampleProvider getDowntimeDurationDistribution() { return downtimeDurationDistribution.getValue(); } public boolean isDown() { return down; } public boolean downtimePending() { return downtimePendings > 0; } @Override public boolean isWatching(StateEntity ent) { if (!this.isActive()) return false; if (iatWorkingEntity.getValue() == ent) return true; if (durationWorkingEntity.getValue() == ent) return true; if (downtimeUserList.contains(ent)) return true; return false; } /** * Return the amount of time in seconds (from the current time) that the next downtime event is due * @return */ public double getTimeUntilNextEvent() { // 1) Calendar time if( iatWorkingEntity.getValue() == null ) { double workingSecs = this.getSimTime(); double waitSecs = secondsForNextFailure - workingSecs; return waitSecs; } // 2) Working time else { if (iatWorkingEntity.getValue().isWorking()) { double workingSecs = iatWorkingEntity.getValue().getWorkingTime(); double waitSecs = secondsForNextFailure - workingSecs; return waitSecs; } } return Double.POSITIVE_INFINITY; } @Override public void updateForStateChange(StateEntity ent, StateRecord prev, StateRecord next) { this.checkProcessNetwork(); } public double getEndTime() { return endTime; } public double getDowntimePendingStartTime() { return downtimePendingStartTime; } public ArrayList<DowntimeUser> getDowntimeUserList() { return downtimeUserList; } // ****************************************************************************************************** // OUTPUTS // ****************************************************************************************************** @Output(name = "StartTime", description = "The time that the most recent downtime event started.", unitType = TimeUnit.class, sequence = 1) public double getStartTime(double simTime) { return startTime; } @Output(name = "EndTime", description = "The time that the most recent downtime event finished or will finish.", unitType = TimeUnit.class, sequence = 2) public double getEndTime(double simTime) { return endTime; } @Output(name = "CalculatedDowntimeRatio", description = "The value calculated directly from model inputs for:\n" + "(avg. downtime duration)/(avg. downtime interval)", sequence = 3) public double getCalculatedDowntimeRatio(double simTime) { if (downtimeDurationDistribution.getValue() == null || downtimeIATDistribution.getValue() == null) return Double.NaN; double dur = downtimeDurationDistribution.getValue().getMeanValue(simTime); double iat = downtimeIATDistribution.getValue().getMeanValue(simTime); return dur/iat; } @Output(name = "Availability", description = "The fraction of calendar time (excluding the initialisation period) during " + "which this type of downtime did not occur.", sequence = 4) public double getAvailability(double simTime) { double total = simTime; if (simTime > Simulation.getInitializationTime()) total -= Simulation.getInitializationTime(); double down = this.getTimeInState(simTime, "Downtime"); return 1.0d - down/total; } }