All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jasima.shopSim.core.WorkStation Maven / Gradle / Ivy

/*******************************************************************************
 * This file is part of jasima, v1.3, the Java simulator for manufacturing and 
 * logistics.
 *  
 * Copyright (c) 2015 		jasima solutions UG
 * Copyright (c) 2010-2015 Torsten Hildebrandt and jasima contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 *******************************************************************************/
package jasima.shopSim.core;

import jasima.core.simulation.Event;
import jasima.core.util.ValueStore;
import jasima.core.util.observer.Notifier;
import jasima.core.util.observer.NotifierAdapter;
import jasima.core.util.observer.NotifierListener;
import jasima.shopSim.core.IndividualMachine.MachineState;
import jasima.shopSim.core.WorkStation.WorkStationEvent;
import jasima.shopSim.core.batchForming.BatchForming;
import jasima.shopSim.core.batchForming.HighestJobBatchingMBS;
import jasima.shopSim.prioRules.basic.FCFS;
import jasima.shopSim.prioRules.basic.TieBreakerFASFS;
import jasima.shopSim.prioRules.meta.IgnoreFutureJobs;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Class to represent a workstation. A workstation is a collection of identical
 * {@link IndividualMachine}s sharing a common queue.
 * 
 * @author Torsten Hildebrandt
 * @version 
 *          "$Id: WorkStation.java 753 2015-07-27 15:29:49Z [email protected] $"
 */
public class WorkStation implements Notifier,
		ValueStore {

	/** Base class for workstation events. */
	public static class WorkStationEvent {
	}

	// constants for default events thrown by a workstation

	public static final WorkStationEvent WS_ACTIVATED = new WorkStationEvent();
	public static final WorkStationEvent WS_DEACTIVATED = new WorkStationEvent();
	public static final WorkStationEvent WS_JOB_ARRIVAL = new WorkStationEvent();
	public static final WorkStationEvent WS_JOB_SELECTED = new WorkStationEvent();
	public static final WorkStationEvent WS_JOB_COMPLETED = new WorkStationEvent();
	public static final WorkStationEvent WS_DONE = new WorkStationEvent();
	public static final WorkStationEvent WS_COLLECT_RESULTS = new WorkStationEvent();
	public static final WorkStationEvent WS_INIT = new WorkStationEvent();

	public static final String DEF_SETUP_STR = "DEF_SETUP";
	public static final int DEF_SETUP = 0;

	/**
	 * Constant to denote the batch family of a job, which is not compatible to
	 * any other.
	 */
	public static final String BATCH_INCOMPATIBLE = "BATCH_INCOMPATIBLE";

	// constants to deterministically sequence concurrent events

	// lookahead-arrivals after selections
	public static final int LOOKAHEAD_PRIO = Event.EVENT_PRIO_LOW;
	public static final int SELECT_PRIO = Event.EVENT_PRIO_NORMAL;
	public static final int DEPART_PRIO = Event.EVENT_PRIO_HIGHER;
	public static final int TAKE_DOWN_PRIO = DEPART_PRIO + 1000; // after depart
	public static final int ACTIVATE_PRIO = DEPART_PRIO - 1000; // before depart

	// parameters

	private String name;
	private final int numInGroup;
	private final IndividualMachine[] machDat;
	private double[][] setupMatrix = { { 0.0 } };

	private BatchForming batchForming;
	private PR batchSequencingRule;

	public final PriorityQueue queue;

	private HashMap valueStore;

	JobShop shop;
	int index; // in shop.machines

	private int numBusy;
	private int numFutures; // number of future arrivals currently in queue

	private boolean batchingUsed;

	// which machine in this group currently selects its next batch? This
	// information is important if, e.g., dispatching rules have to determine a
	// specific machine's setup state
	public IndividualMachine currMachine;

	ArrayDeque freeMachines;
	// reuse select event objects
	private SelectEvent selectEventCache;

	private final class SelectEvent extends Event {
		private SelectEvent(double time) {
			super(time, SELECT_PRIO);
		}

		@Override
		public void handle() {
			// are there jobs that could be started and is there at
			// least one free machine
			if (numBusy < numInGroup && numJobsWaiting() > 0) {
				// at least 1 machine idle, start job selection
				selectAndStart0();
			}

			// reuseEvent(this);
			this.next = selectEventCache;
			selectEventCache = this;
		}

		protected SelectEvent next = null; // linked list, see selectEventCache
	}

	protected double workContentReal, workContentFuture;
	private ArrayList setupStateTranslate;
	private Map> jobsPerBatchFamily;

	// the following fields temporarily contain parameters used by listeners
	public Job justArrived;
	public PrioRuleTarget justStarted;
	public PrioRuleTarget justCompleted;
	public int oldSetupState;
	public int newSetupState;
	public double setupTime;
	public Map resultMap;

	public WorkStation() {
		this(1);
	}

	public WorkStation(int numInGroup) {
		super();
		this.valueStore = null;
		this.numInGroup = numInGroup;

		numBusy = 0;
		queue = new PriorityQueue(this);

		PR sr = new FCFS();
		sr.setTieBreaker(new TieBreakerFASFS());
		sr.setOwner(this);
		queue.setSequencingRule(new IgnoreFutureJobs(sr));

		batchForming = new HighestJobBatchingMBS();
		batchForming.setOwner(this);

		machDat = new IndividualMachine[numInGroup];
		for (int i = 0; i < machDat.length; i++) {
			machDat[i] = new IndividualMachine(this, i);
		}
	}

	public void init() {
		assert translateSetupState(DEF_SETUP_STR) == DEF_SETUP;

		batchingUsed = false;
		jobsPerBatchFamily = null;

		workContentFuture = workContentReal = 0.0d;

		freeMachines = new ArrayDeque(numInGroup);
		numBusy = 0;

		queue.clear();
		selectEventCache = null;
		currMachine = null;

		for (int i = machDat.length - 1; i >= 0; i--) {
			IndividualMachine imd = machDat[i];
			imd.init();
			numBusy++;
		}

		queue.getSequencingRule().init();

		if (getBatchSequencingRule() != null)
			getBatchSequencingRule().init();

		if (numListener() > 0) {
			fire(WS_INIT);
		}
	}

	void activated(IndividualMachine im) {
		assert currMachine == im;
		freeMachines.addFirst(currMachine);

		numBusy--;
		assert numBusy >= 0 && numBusy <= numInGroup;

		// start a job on this machine
		if (numJobsWaiting() > 0)
			selectAndStart();

		if (numListener() > 0) {
			fire(WS_ACTIVATED);
		}
	}

	void takenDown(IndividualMachine im) {
		assert currMachine == im;
		freeMachines.remove(currMachine);

		numBusy++;
		assert numBusy >= 0 && numBusy <= numInGroup;

		if (numListener() > 0) {
			fire(WS_DEACTIVATED);
		}
	}

	public void done() {
		selectEventCache = null;

		if (numListener() > 0) {
			fire(WS_DONE);
		}
	}

	public void produceResults(Map res) {
		resultMap = res;
		fire(WS_COLLECT_RESULTS);
		resultMap = null;
	}

	/**
	 * Job 'j' arrives at a machine.
	 */
	public void enqueueOrProcess(Job j) {
		assert this == j.getCurrentOperation().machine;
		assert !j.isFuture();

		// remove the job's future from the queue if present
		if (numFutures > 0) {
			removeFromQueue(j.getMyFuture());
		}

		addToQueue(j, shop.simTime());
	}

	/**
	 * The machine is notified of the future arrival of the job {@code f} at a
	 * certain time. Note that f is not the job itself but a clone of this job
	 * with current operation advanced to this machine obtained with
	 * {@link Job#getFuture()}.
	 */
	public void futureArrival(final Job f, final double arrivesAt) {
		// execute asynchronously a little later so exactly concurrent job
		// selections don't see each others results
		shop.schedule(new Event(shop.simTime(), LOOKAHEAD_PRIO) {

			@Override
			public void handle() {
				assert f.isFuture();
				assert !queue.contains(f);
				addToQueue(f, arrivesAt);
			}
		});
	}

	private void addToQueue(Job j, double arrivesAt) {
		j.arriveInQueue(this, arrivesAt);

		queue.add(j);
		Operation o = j.getCurrentOperation();
		if (!batchingUsed) {
			batchingUsed = !BATCH_INCOMPATIBLE.equals(o.batchFamily);
		}

		if (!j.isFuture()) {
			workContentReal += o.procTime;
		} else {
			numFutures++;
			workContentFuture += o.procTime;
		}

		if (jobsPerBatchFamily != null)
			addJobToBatchFamily(j);

		if (numListener() > 0) {
			justArrived = j;
			fire(WS_JOB_ARRIVAL);
			justArrived = null;
		}

		// are there jobs that could be started and at least a free
		// machine
		if (numBusy < numInGroup && numJobsWaiting() > 0) {
			// at least 1 machine idle, start job selection
			selectAndStart();
		}
	}

	public void removeFromQueue(Job j) {
		boolean removeRes = queue.remove(j);
		j.removedFromQueue();

		if (!j.isFuture()) {
			Operation o = j.getCurrentOperation();
			workContentReal -= o.procTime;
			assert workContentReal >= -1e-6 : "" + workContentReal;
			assert removeRes;
			if (jobsPerBatchFamily != null)
				removeJobOfBatchFamily(j, o.batchFamily);
		} else {
			if (removeRes) {
				Operation o = j.getOps()[j.getTaskNumber() - 1];
				workContentFuture -= o.procTime;
				assert workContentFuture >= -1e-6 : "" + workContentFuture;
				numFutures--;
				if (jobsPerBatchFamily != null)
					removeJobOfBatchFamily(j, o.batchFamily);
			}
		}
	}

	/**
	 * Start processing the current batch/job.
	 */
	protected void startProc(final PrioRuleTarget batch) {
		assert !batch.isFuture();
		for (int i = 0; i < batch.numJobsInBatch(); i++) {
			assert !queue.contains(batch.job(i).getMyFuture());
		}
		assert numBusy < numInGroup;
		assert batch.getCurrentOperation().machine == this;
		assert currMachine.state == MachineState.IDLE;

		double simTime = shop.simTime();
		freeMachines.remove(currMachine);

		// remove job/batch's jobs from queue
		for (int i = 0; i < batch.numJobsInBatch(); i++) {
			Job job = batch.job(i);
			removeFromQueue(job);
		}

		// at least 1 machine idle, start job
		numBusy++;

		Operation op = batch.getCurrentOperation();

		oldSetupState = currMachine.setupState;
		newSetupState = op.setupState;
		setupTime = 0.0;
		if (oldSetupState != newSetupState) {
			setupTime = setupMatrix[oldSetupState][newSetupState];
			currMachine.setupState = newSetupState;
		}

		double tCompl = simTime + op.procTime + setupTime;
		currMachine.onDepart.setTime(tCompl);
		currMachine.procFinished = tCompl;
		currMachine.procStarted = simTime;
		currMachine.curJob = batch;
		shop.schedule(currMachine.onDepart);

		for (int i = 0; i < batch.numJobsInBatch(); i++) {
			Job j = batch.job(i);
			j.startProcessing();
		}

		currMachine.state = MachineState.WORKING;
	}

	/** Called when an operation of Job j is finished. */
	protected void depart() {
		assert currMachine.state == MachineState.WORKING;

		PrioRuleTarget b = currMachine.curJob;
		currMachine.curJob = null;

		currMachine.state = MachineState.IDLE;
		currMachine.procFinished = -1.0d;
		currMachine.procStarted = -1.0d;
		freeMachines.addFirst(currMachine);

		numBusy--;

		if (numListener() > 0) {
			justCompleted = b;
			fire(WS_JOB_COMPLETED);
			justCompleted = null;
		}

		for (int i = 0, n = b.numJobsInBatch(); i < n; i++) {
			Job j = b.job(i);
			j.endProcessing();
			// send jobs to next machine
			j.proceed();
		}

		currMachine = null;

		// start next job on this machine
		if (numJobsWaiting() > 0)
			selectAndStart();
	}

	/**
	 * Selects the next batch from the queue and starts processing. Even though
	 * this method is public it should never be called externally unless you
	 * know exactly what you are doing.
	 */
	public void selectAndStart() {
		// reuse select event objects
		SelectEvent e = selectEventCache;
		if (e == null) {
			e = new SelectEvent(shop.simTime());
		} else {
			selectEventCache = e.next;
			e.setTime(shop.simTime());
		}

		// execute asynchronously so all jobs arrived/departed before selection
		shop.schedule(e);
	}

	protected void selectAndStart0() {
		assert numJobsWaiting() > 0;
		assert numBusy < numInGroup;

		PrioRuleTarget nextBatch = nextJobAndMachine();
		assert freeMachines.contains(currMachine);

		// start work on selected job/batch
		if (nextBatch != null) {
			startProc(nextBatch);
		}

		// inform listener
		if (numListener() > 0) {
			justStarted = nextBatch;
			fire(WS_JOB_SELECTED);
			justStarted = null;
		}

		currMachine = null;
	}

	protected PrioRuleTarget nextJobAndMachine() {
		// just a check if freeMachines contains the right data
		for (IndividualMachine md : machDat) {
			if (md.state == MachineState.IDLE)
				assert freeMachines.contains(md);
		}
		assert freeMachines.size() > 0;

		PrioRuleTarget maxJob;

		if (!batchingUsed) {
			// normal job
			currMachine = freeMachines.peekLast();
			maxJob = queue.peekLargest();

			if (freeMachines.size() > 1) {
				// more than a single machine are free, we have to check them
				// all
				IndividualMachine maxMachine = currMachine;
				double[] maxPrio = queue.getBestPrios();
				if (maxPrio != null)
					maxPrio = maxPrio.clone();

				Iterator it = freeMachines
						.descendingIterator();
				// skip first entry, which was already considered
				it.next();
				while (it.hasNext()) {
					currMachine = it.next();

					Job job = queue.peekLargest();
					double[] prios = queue.getBestPrios();

					if (maxPrio == null
							|| PriorityQueue.comparePrioArrays(maxPrio, prios) >= 0) {
						// copy priorities
						maxPrio = prios;
						if (maxPrio != null)
							maxPrio = maxPrio.clone();
						// remember job and machine
						maxJob = job;
						maxMachine = currMachine;
					}
				}

				currMachine = maxMachine;
			}
		} else {
			// batch machine

			// TODO: check all free machines similarly to normal jobs; only
			// makes a difference if batch machines can have setups too
			currMachine = freeMachines.peekLast();
			maxJob = getBatchForming().nextBatch();
		}

		if (maxJob == null || maxJob.isFuture()) {
			return null;
		} else {
			return maxJob;
		}
	}

	/**
	 * Return the number of jobs waiting in {@link #queue}, ready to be started
	 * immediately. This does not include the KeepIdleDummy. This means, the
	 * following equation holds: queue.size()=={@link #numJobsWaiting()}+
	 * {@link #numFutures()}+1.
	 * 
	 * @see #numFutures()
	 */
	public int numJobsWaiting() {
		int res = queue.size() - numFutures;
		return res;
	}

	/**
	 * Returns the number of future jobs in the {@link #queue}. This does not
	 * include the KeepIdleDummy.
	 * 
	 * @see #numJobsWaiting()
	 */
	public int numFutures() {
		return numFutures;
	}

	/**
	 * How much work have all machines in this group to finish their current
	 * jobs.
	 */
	public double startedWorkInGroup() {
		double res = 0.0;
		for (int i = 0; i < machDat.length; i++) {
			if (machDat[i].procFinished > shop.simTime())
				res += (machDat[i].procFinished - shop.simTime());
		}
		return res;
	}

	public double againIdleIn() {
		assert numInGroup == 1;
		return startedWorkInGroup();
	}

	public double againIdle() {
		return againIdleIn() + shop.simTime();
	}

	/**
	 * Returns the sum of processing times of all operations currently waiting
	 * in this machine's queue.
	 */
	public double workContent(boolean includeFutureJobs) {
		// jobs already started
		double res = startedWorkInGroup() + workContentReal;

		if (includeFutureJobs)
			res += workContentFuture;

		// normalize with machine group size
		return res / numInGroup;
	}

	public PrioRuleTarget getProcessedJob(int machIdx) {
		return machDat[machIdx].curJob;
	}

	public int getSetupState(int machIdx) {
		return machDat[machIdx].setupState;
	}

	public void setSetupMatrix(double[][] setupMatrix) {
		this.setupMatrix = setupMatrix;
	}

	public double[][] getSetupMatrix() {
		return setupMatrix;
	}

	/**
	 * Translates a setup state {@code s} in a numeric constant.
	 * 
	 * @see #setupStateToString(int)
	 * @param s
	 *            A setup state name.
	 * @return Numeric constant for {@code s}.
	 */
	public int translateSetupState(String s) {
		if (DEF_SETUP_STR.equals(s)) {
			return DEF_SETUP;
		} else {
			if (setupStateTranslate == null) {
				setupStateTranslate = new ArrayList();
				setupStateTranslate.add(DEF_SETUP_STR); // ensure an index of 0
			}
			int i = setupStateTranslate.indexOf(s);
			if (i < 0) {
				i = setupStateTranslate.size();
				setupStateTranslate.add(s);
			}
			return i;
		}
	}

	/**
	 * Provides a human-readable string for a numeric setup state.
	 * 
	 * @see #translateSetupState(String)
	 * @param id
	 *            The numeric setup id. This was usually (optionally) created
	 *            before using {@code translateSetupState(String)}
	 */
	public String setupStateToString(int id) {
		if (id == DEF_SETUP)
			return DEF_SETUP_STR;
		else {
			if (setupStateTranslate != null & id >= 0
					&& id < setupStateTranslate.size())
				return setupStateTranslate.get(id);
			else
				return "sId" + id;
		}
	}

	public int numFreeMachines() {
		return freeMachines.size();
	}

	public Collection getFreeMachines() {
		return Collections.unmodifiableCollection(freeMachines);
	}

	public IndividualMachine[] machDat() {
		return machDat;
	}

	public int numBusy() {
		return numBusy;
	}

	public int numInGroup() {
		return numInGroup;
	}

	public JobShop shop() {
		return shop;
	}

	public int index() {
		return index;
	}

	public Map> getJobsByFamily() {
		if (jobsPerBatchFamily == null) {
			jobsPerBatchFamily = new HashMap>();
			for (int i = 0, n = queue.size(); i < n; i++) {
				Job j = queue.get(i);
				addJobToBatchFamily(j);
			}
		}
		return jobsPerBatchFamily;
	}

	private void addJobToBatchFamily(Job j) {
		String bf = j.getCurrentOperation().batchFamily;

		List jobsInFamily = jobsPerBatchFamily.get(bf);
		if (jobsInFamily == null) {
			jobsInFamily = new ArrayList();
			jobsPerBatchFamily.put(bf, jobsInFamily);
		}

		jobsInFamily.add(j);
	}

	private void removeJobOfBatchFamily(Job j, String bf) {
		List jobsInFamily = jobsPerBatchFamily.get(bf);
		boolean removeRes = jobsInFamily.remove(j);
		assert removeRes;
	}

	@Override
	public String toString() {
		return getName();
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name == null ? "m" + index : name;
	}

	public void setBatchForming(BatchForming formBatch) {
		this.batchForming = formBatch;
		if (formBatch != null)
			formBatch.setOwner(this);
	}

	public BatchForming getBatchForming() {
		return batchForming;
	}

	public void setBatchSequencingRule(PR batchSequencingRule) {
		this.batchSequencingRule = batchSequencingRule;
		if (batchSequencingRule != null)
			batchSequencingRule.setOwner(this);
	}

	public PR getBatchSequencingRule() {
		return batchSequencingRule;
	}

	//
	//
	// event notification
	//
	//

	private NotifierAdapter adapter = null;

	@Override
	public void addNotifierListener(
			NotifierListener listener) {
		if (adapter == null)
			adapter = new NotifierAdapter(this);
		adapter.addNotifierListener(listener);
	}

	@Override
	public NotifierListener getNotifierListener(
			int index) {
		return adapter.getNotifierListener(index);
	}

	@Override
	public void removeNotifierListener(
			NotifierListener listener) {
		adapter.removeNotifierListener(listener);
	}

	@Override
	public int numListener() {
		return adapter == null ? 0 : adapter.numListener();
	}

	protected void fire(WorkStationEvent event) {
		if (adapter != null)
			adapter.fire(event);
	}

	/**
	 * Offers a simple get/put-mechanism to store and retrieve information as a
	 * kind of global data store. This can be used as a simple extension
	 * mechanism.
	 * 
	 * @param key
	 *            The key name.
	 * @param value
	 *            value to assign to {@code key}.
	 * @see #valueStoreGet(Object)
	 */
	@Override
	public void valueStorePut(Object key, Object value) {
		if (valueStore == null)
			valueStore = new HashMap();
		valueStore.put(key, value);
	}

	/**
	 * Retrieves a value from the value store.
	 * 
	 * @param key
	 *            The entry to return, e.g., identified by a name.
	 * @return The value associated with {@code key}.
	 * @see #valueStorePut(Object, Object)
	 */
	@Override
	public Object valueStoreGet(Object key) {
		if (valueStore == null)
			return null;
		else
			return valueStore.get(key);
	}

	/**
	 * Returns the number of keys in the value store.
	 */
	@Override
	public int valueStoreGetNumKeys() {
		return (valueStore == null) ? 0 : valueStore.size();
	}

	/**
	 * Returns a list of all keys contained in the value store.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Set valueStoreGetAllKeys() {
		if (valueStore == null)
			return Collections.EMPTY_SET;
		else
			return valueStore.keySet();
	}

	/**
	 * Removes an entry from the value store.
	 * 
	 * @return The value previously associated with "key", or null, if no such
	 *         key was found.
	 */
	@Override
	public Object valueStoreRemove(Object key) {
		if (valueStore == null)
			return null;
		else
			return valueStore.remove(key);
	}

	@SuppressWarnings("unchecked")
	@Override
	protected Object clone() throws CloneNotSupportedException {
		WorkStation ws = (WorkStation) super.clone();

		if (valueStore != null) {
			ws.valueStore = (HashMap) valueStore.clone();
		}

		if (adapter != null) {
			ws.adapter = adapter.clone();
			ws.adapter.setNotifier(ws);
		}

		return ws;
	}

}