Tutorial¶
The system¶
Let’s assume, that our production system scheme is presented below:
+----------+ +---------+ +--------+
| Raw | | | | |
| material |---------| Machine |---------| Market |
| | | | | |
+----------+ +---------+ +--------+
We own a machine, which turns some raw material into a product, which is then sold on the market.
Each unit of the raw material has its cost, and we assume, that we can buy as many of them, as we want. The only limitation is our cash.
On the other hand, we can sell our products to the market, for some price. We assume no limitations on the amount of sold products.
And, finally, raw material units are processed on the machine, and turned into products. But before any processing can be performed, the machine should be prepared during the setup process.
The Model¶
A JSN net is composed of places, transitions and arcs. Places are used to model the state of system, transitions are used to model actions (which change the state), and arcs connect places to transitions. In order to create a model for our system, we have to analyse the state of the system and all the possible actions.
Let’s start with the raw material. We know, that raw material units are bought, for some price. After a unit of the raw material is bought, it can be processed on the machine in order to produce a unit of the final product. So, we can conclude that:
- after buying a unit of the raw material, the number of the raw material units in the input machine buffer is increased by one,
- after buying a unit of the raw material, the amount of cash is decreased by the unit’s price.
So, the action of buying a unit of the raw material should affect the state of our system in two ways: it should increase the number of the raw material units in the input buffer of our machine, and it should decrease the amount of our cash. We can conclude, that the model should consist of (at least) two places: one for the machine, and one for the cash.
Now, let’s think about the machine. So far, we know that it can store the raw material’s units in its input buffer. It can also produce the final products, but before it can be done, the machine should be set up. We can point out the following features of the machine:
- after setting up the machine is ready to produce the final products, provided that at least one unit of the raw material is in its input buffer,
- moreover, we could also assume, that once the machine is ready to produce, and this is at least one unit of the raw material in its input buffer, the production should start immediatelly,
- once the machine is set up, but no raw material units are available, the machine remains idle, until a raw material unit is bought,
- when the final product is ready, it should be immediatelly sold to the market,
- after the production, the machine can be both idle (when no raw material units are available), or should start producing the next unit of the final product (otherwise).
First of all, the machine model gets more complex. It is clear now, that the machine’s state can be either “not ready”, or “set up”, or “idle”, or “busy”.
With such assumptions, we can conclude, that the model should consist of one more place: the market.
We can also point out the following actions: setting up the machine and starting the production of the final product. Setting up the machine should cause the state of the machine to change from “not ready” to “set up”, and, after some time, to either “idle” or “busy” (depending on the number of available units of the raw material).
And once the final product is ready, it should be sold out to the market. So, our model should consist of one more transition: “sell”. This transition should affect the number of available final products to decrease, and the number of sold final products to increase, and also the amount of cash to increase.
Right now, when we have some idea on what places and transitions the model should consist of, we can start working on the tests for such a model.
The Tests¶
Well, let’s start with the skeleton for our tests:
describe( 'Single machine model', () => {
let net;
beforeEach( () => {
net = require('../build/js/model.js').model();
});
let checkPlace = (name, value) => {
let place = net.getPlace(name).token;
place.should.eql(value);
};
The beforeEach function is supposed to load our model from an external Node.js module. This function will be called, well, before each test, so each test will start with a fresh model.
Also an auxiliary finction, checkPlace, has been defined. This function will be used to examine the cash and market places, used in our model to store the state of the cash and the number of the sold final products.
Now, let’s start working on the tests. First, we can check the initial state:
it( 'initial state', () => {
// the initial amount of cash
checkPlace('cash', 10);
// the initial state of the machine
let place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('state', 'not-prepared');
// the initial state of the market
checkPlace('market', 0);
// the initial state of all the transitions
net.isReady('buy').should.be.true;
net.isReady('setup').should.be.true;
net.isReady('produce').should.be.false;
net.isReady('sell').should.be.false;
// the initial time
net.getTime().should.eql(0);
});
There’s a lot to be explained here.
I suppose that checking the initial states of the cash and market places need no special explanation.
Note
Some more advanced questions to consider:
- Is the
marketplace really needed? Shouldn’t the number of sold final products be kept in themachineplace instead? So why this place was introduced into the model? - The same question is also valid for the amount of cash. Why use a separate place, if the information can be kept in the
machineplace?
If you can’t answer those questions, don’t worry. You don’t have to be able to answer them in order to use the JSN library, and in order to make your own models.
The initial state of the machine should be not prepared to produce anything (the machine requires to be set up), and the input buffer should contain no units of the raw material.
The buy and setup transitions should be ready, and the produce and sell ones shouldn’t (since the machine hasn’t been set up yet, and there are no final products to be sold).
The time of the model should be 0.
Now, let’s assume, that a unit of the raw material has been bought. What should happen then?
it( 'buying the raw material', () => {
// buy a unit of the raw material
net.fire('buy');
// check the amount of cash
checkPlace('cash', 5);
// check the state of the machine
let place = net.getPlace('machine').token;
place.should.have.property('in', 1);
place.should.have.property('state', 'not-prepared');
// the market shouldn't be affected at all
checkPlace('market', 0);
// the transitions also shouldn't be affected at all
net.isReady('buy').should.eql(true);
net.isReady('setup').should.eql(true);
net.isReady('produce').should.eql(false);
// and the time also shouldn't be affected
net.getTime().should.eql(0);
});
Please note, that the test starts with firing the buy transition. No checks are performed for the transition: it is simply assumed, that it is ready. Its readiness has been tested in the first test, and there’s no reason to do it again here.
After a unit of the raw material has been bought, the amount of cash should decrease by 5. In this test it’s assumed, that the unit price of the raw material is equal to 5.
Buying the raw material should affect the machine. The number of the raw material units in the input buffer should increase, but the machine’s state should remain unchanged.
The market, as well as all the transitions and the model time, should remain unchanged also.
Note
Some tests may seem redundant. In fact, it is rather unlikely, that buying a unit of the raw material could affect the market, or the state of the machine.
I always try to be as defensive, as possible, while testing. That’s why I propose to test almost every single feature after a single operation. I never know, when I’ll start to hack the code (may it never happen!), and break something while trying to write some very smart piece of code. Such defensive testing helps me to prevent such situations (even if they almost never occur).
After such a test, let’s try to buy all the raw material units we can afford:
it( 'spend all the cash on raw materials', () => {
// buy two units of the raw material
net.fire('buy');
net.fire('buy');
// check the amount of cash
checkPlace('cash', 0);
// check the state of the machine
let place = net.getPlace('machine').token;
place.should.have.property('in', 2);
place.should.have.property('state', 'not-prepared');
// the market shouldn't be affected at all
checkPlace('market', 0);
// since we spent all the cash, buying shouldn't be possible anymore
net.isReady('buy').should.eql(false);
// the other transitions shouldn't be affected at all
net.isReady('setup').should.eql(true);
net.isReady('produce').should.eql(false);
});
After all the cash has been spent on the raw material, no cash should be available.
The only changes to the previous test are the state of the machine (more units of the raw material has been bought) and the buy transition shouldn’t be ready anymore (since this is no cash that could be paid for more units of the raw material). Everything else is the same.
Now, let’s get to a more complex task: let’s set up the machine:
it( 'setup the machine', () => {
// setup the machine
net.fire('setup');
// the machine's state should've been affected
let place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('state', 'setup');
place.should.have.property('time', 5);
// the market and cash shouldn't be affected at all
checkPlace('cash', 10);
checkPlace('market', 0);
// wait until the machine is setup
net.tick(); // the net time is now 1
// after each tick, the machine's timer should be decreased
place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('state', 'setup');
place.should.have.property('time', 4);
// the market and cash shouldn't be affected at all
checkPlace('cash', 10);
checkPlace('market', 0);
net.tick(); // the net time is now 2
net.tick(); // the net time is now 3
net.tick(); // the net time is now 4
net.tick(); // the net time is now 5
// now, the machine should be set up
place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('state', 'idle');
});
First of all, let’s note, that after setup has been started, the state of the machine has changed. Besides the change of the state property, a new property, time, has been set to 5. It means, that 5 time units are needed to complete the set up task. After each tick, this property value should decrease. And this behaviour is also tested, after the first tick.
When the set up is ready, the machine’s state should be “idle”, since no units of the raw material are available.
The next test is the first one, where the production is checked:
it( 'produce the product', () => {
// set up the machine
net.fire('setup');
net.tick();
net.tick();
net.tick();
net.tick();
net.tick();
// buy a unit of raw material
net.fire('buy');
// the production should've been started
let place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('out', 0);
place.should.have.property('state', 'busy');
place.should.have.property('time', 2);
// the state of the market shouldn't have been affected at all
checkPlace('market', 0);
// wait until the production is done
net.tick();
net.tick();
// now, the machine should be idle
place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('out', 0);
place.should.have.property('state', 'idle');
// and one unit of the final product should've been sold
checkPlace('market', 1);
checkPlace('cash', 15);
});
The test starts with the set up operation. The results of set up are not tested at all, since they have been tested in the previous test.
Right after the machine is set up (and ready to produce), a unit of the raw material is bought. According to our model specification, when a machine is set up, and a unit of the raw material is available, the production should immediatelly start. So the state of the machine should be set to “busy”. The time of the production is set to 2 units of time.
The results of the production are also tested. Once the final product is ready, it should be immediatelly sold. So, the number of final products in the output buffer of our machine should be equal to 0. The state of the market should increase by 1, and the amount of cash should increase by 10 (the assumed price of the final product).
The last test checks a different scenario: a unit of the raw material is bought, and then the machine is set up. According to the model specification, once the machine is ready (set up), the production should start:
it( 'start production as soon as the machine is ready', () => {
// buy a unit of raw material
net.fire('buy');
// set up the machine
net.fire('setup');
net.tick();
net.tick();
net.tick();
net.tick();
net.tick();
// the production should've been started
let place = net.getPlace('machine').token;
place.should.have.property('in', 0);
place.should.have.property('out', 0);
place.should.have.property('state', 'busy');
place.should.have.property('time', 2);
});
});
All the tests are ready. You can prepare a file with all the code and try to run them. Of course, all of them should fail - and that’s good, since the model hasn’t been implemented yet. Now, it’s time to implement the model, in order to pass all the tests.
The Model Implementation¶
Let’s start with a module skeleton.
Note
Without a model module, the tests fail due to a simple reason: the net model cannot be created, and the beforeEach function cannot work at all.
Warning
I haven’t tested the code by myself. If it’s broken, please let me know.
In the skeleton, the JSN library will be loaded, and the function that creates the model should be exported:
var jsn = require('jsn');
exports.model = function() {
return jsn.Net();
};
The JSN model is a specification, in a form of a JavaScript object. This object should be passed to the Net function. Let’s try to create the specification.
First, let’s focus on the cash, since it’s easier (why it can’t be as easy in the real life? ;-)). This place should have only one property: the amount of available cash:
{
places: [
{ name: 'cash', token: 10 }
]
}
Since the amount of cash is simply a number, there’s no need for using more complex JavaScript object; a simple number can be used. (Besides, the tests expect the cash place token to be a number...)
The second place represents the machine. After initialisation, according to the tests, this place should consist of (at least) two properties: in set to 0, and state set to “not-prepared”:
{
places: [
{ name: 'cash', token: 10 },
{ name: 'machine', token: { in: 0, state: 'not-prepared' } }
]
}
And, finally, we need a market place, with token of the number of final products sold:
{
places: [
{ name: 'cash', token: 10 },
{ name: 'machine', token: { in: 0, state: 'not-prepared' } },
{ name: 'market', token: 0 }
]
}
The first test doesn’t need any functionality: it merely checks for the presence of some places (and their properties), and for readiness of some transitions. Yet, no transition is fired. So, we won’t focus on the model’s functionality - we will simply add all the needed transitions and make sure, they are either ready or not.
{
places: [
{ name: 'cash', token: 10 },
{ name: 'machine', token: { in: 0, state: 'not-prepared', timer: null } },
{ name: 'market', token: 0 }
],
transitions: [
{ name: 'buy' },
{ name: 'setup' },
{ name: 'produce' },
{ name: 'sell' }
],
arcs: [
{ place: 'cash', transition: 'buy',
evaluate: {
ready: true,
place: (cash) => cash
} },
{ place: 'machine', transition: 'setup',
evaluate: {
ready: true,
place: (token) => token
} },
{ place: 'machine', transition: 'produce',
evaluate: {
ready: false,
place: (token) => token
} },
{ place: 'market', transition: 'sell',
evaluate: {
ready: false,
place: (sold) => sold
} }
]
}
Note
If you think that the implementation presented above is broken, you’re right. It is.
For example, the buy transition doesn’t decrease the amount of cash, and doesn’t affect the number of units of the raw material in the machine’s input buffer.
Why is it so? Because, in order to pass the first test, no functionality is actually needed. What is needed are the transitions and they should be either ready (buy, setup), or not (produce, sell). So, the simplest way to pass this test is to provide such minimalistic implementation. We don’t have to worry about the current, incorrect implementation. We have other tests to check for the correctness.
Right now, it should be possible to prepare the files and run the test. (Just remember, that all of the code is written in ES6, so if you use Node.js that doesn’t support ES6, you have to transpile the code using babel, or any other ES6 transpiler.)
The first test should pass right now, but the others are failing. That’s because no functionality has been implemented so far. Let’s try to fix (pass) the second test, while keeping the first one still passing.
The second test starts with buying a unit of the raw material. According to the model (and to the second test, which follows the model specification), such an action should affect the amount of cash (the price of a unit of the raw material is 5) and the number of the raw material units in the machine’s input buffer. So, the transition buy should be connected to the cash place and to the machine one.
Let’s try to fix the connection (the arc) between the buy transition and the cash place:
arcs: [
{ place: 'cash', transition: 'buy',
evaluate: {
ready: (cash) => cash >= 5,
place: (cash) => cash - 5
} },
Please note, that a condition has been introduced to check the readiness of this arc: it is ready, provided that the amount of cash is at least 5.
The place function simply takes 5 units of cash from the cash place - it’s paying for a single unit of the raw material.
But that’s not all, since the connection between the buy transition and the machine place is still needed:
arcs: [
{ place: 'cash', transition: 'buy',
evaluate: {
ready: (cash) => cash >= 5,
place: (cash) => cash - 5
} },
{ place: 'machine', transition: 'buy',
evaluate: {
ready: true,
update: {
place: (t) => {
return {
in: t.in + 1
};
}
}
} },
Two things to note here.
Let’s ask the following question: what is the condition for readiness of the buy transition? The correct answer to this question is: the buy transition is ready, when the amount of cash is sufficient to buy a unit of the raw material. The reason is simple: the buy transition is connected to two places, and only one of those connections has the ready expression, which can be either true or false.
The second thing to note is the update expression. Instead of replacing the machine’s token, its value is updated: the in property is simply increased. Updating token’s value can be handy, when some transition should affect only some of all its properties.
After such modification, the second test should pass. Also the third one should pass.
Now, it’s time to set up the machine.
The setup operation on the machine takes some time. It means, that the results of this operation should affect the state of the machine in 5 units of time since it was started. Such a behaviour can be modeled in two ways, using the JSN library: timed tokens could be used, or some kind of internal machine timer.
In this tutorial, the latter approach will be applied. The reason is simple: using the first approach, there would be no way to find out, how much time left to complete the ongoing operation. (The second reason is that it would be impossible to cancel the operation.) Provided we have such “internal” timer in the machine place, it’s very easy to control the time left to finish any ongoing operation.
arcs: [
// (...)
{ place: 'machine', transition: 'setup',
evaluate: {
ready: (t) => t.state === 'not-prepared',
update: {
place: (t) => {
return {
state: 'setup',
time: 5
};
}
}
} },
Looks fine, but that’s not all. The “internal” timer is set, but it should decrease on every net timer tick. Moreover, when the internal timer is finished, the machine should be ready to work.
So far, no JSN features have been introduced, that would allow to achieve such automatic functionality. And that’s the reason why so-called transition events have been introduced to JSN.
Transition events are JavaScript functions, which are called when some specific event occurs. So far, there are three different transition events: onReady, onTick and onFired. The onReady event occurs, when the transition is ready to be fired. The onTick event occurs whenever the net timer is increased. The onFired event occurs after the transition was fired.
In the case of the internal timer, a transition to decrease the timer is needed, and this transition should be fired whenever the internal timer is set and the net timer ticks:
transitions: [
// (...)
{ name: 'tick-setup', onTick: 'tick-setup' }
],
arcs: [
// (...)
{ place: 'machine', transition: 'tick-setup',
evaluate: {
ready: (t) => (t.timer !== null) && (t.state === 'setup'),
update: {
place: (t) => {
if ( t.timer > 1 )
return { timer: t.timer - 1 };
else
return { timer: null, state: 'idle' };
}
}
} }
]
The notation onTick: 'tick-setup' is a convenient way to say “after the net timer ticks, fire the transition tick-setup, provided it is ready to be fired”.
Firing this transition causes the timer to be either decreased, or to be set to null and to set the state of the machine to “idle”.
Now, when there’s a way to set up the machine, the next functionality should be producing the final product. Please note, that starting production should be automatic. Let’s remind the conditions of starting the production:
- the machine is ready to start the production,
- this is at least one unit of the raw material in the machine’s input buffer.
The first condition is met when the machine’s state is “idle”. The second condition is met, when the in property of the machine place is higher than one.
The setup transition was meant to be fired manually. The produce transition should be fired automatically, when the conditions mentioned above are met. But when should those conditions be checked? The answer is: whenever the place machine is updated. For such purpose, the place event onUpdate can be used:
{
places: [
// (...)
{ name: 'machine', token: { in: 0, state: 'not-prepared', timer: null },
onUpdate: 'produce' },
// (...)
],
transitions: [
// (...)
{ name: 'produce' },
// (...)
],
arcs: [
// (...)
{ place: 'machine', transition: 'produce',
evaluate: {
ready: (t) => (t.state === 'idle') && (t.in > 0),
update: {
place: (t) => {
return {
state: 'busy',
timer: 2,
in: t.in - 1
};
}
}
} }
]
Of course, since the internal timer is used, a transition to tick it is also needed:
{
transitions: [
// (...)
{ name: 'tick-production', onTick: 'tick-production' },
// (...)
],
arcs: [
// (...)
{ place: 'machine', transition: 'tick-production',
evaluate: {
ready: (t) => (t.state === 'busy') && (t.timer !== null),
update: {
place: (t) => {
if ( t.timer > 1 )
return {
timer: t.timer - 1
};
else
return {
timer: null,
out: t.out + 1
};
}
}
} }
// (...)
]
Note
In the implementation presented above, two different transition are used for all “ticking”: tick-setup and tick-production. Try to implement their functionality in one transition, named tick. What are the pros and cons of both solutions?
The implementation is not complete yet: the produced final product should be immediatelly sold to the maket. This further improvement of the model is left to the reader, as an exercise.