You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
18 KiB
682 lines
18 KiB
//==============================================================================
|
|
//
|
|
// Copyright (c) 2002-
|
|
// Authors:
|
|
// * Dave Parker <david.parker@comlab.ox.ac.uk> (University of Oxford)
|
|
// * Christian von Essen <christian.vonessen@imag.fr> (Verimag, Grenoble)
|
|
//
|
|
//------------------------------------------------------------------------------
|
|
//
|
|
// This file is part of PRISM.
|
|
//
|
|
// PRISM is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// PRISM 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 General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with PRISM; if not, write to the Free Software Foundation,
|
|
// Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
//
|
|
//==============================================================================
|
|
|
|
package explicit;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.File;
|
|
import java.io.FileReader;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.BitSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map.Entry;
|
|
|
|
import prism.PrismException;
|
|
|
|
|
|
/**
|
|
* Simple explicit-state representation of an MDP.
|
|
* The implementation is far from optimal, both in terms of memory usage and speed of access.
|
|
* The model is, however, easy to manipulate. For a static model (i.e. one that does not change
|
|
* after creation), consider MDPSparse, which is more efficient.
|
|
*/
|
|
public class MDPSimple extends MDPExplicit implements NondetModelSimple
|
|
{
|
|
// Transition function (Steps)
|
|
protected List<List<Distribution>> trans;
|
|
|
|
// Action labels
|
|
// (null list means no actions; null in element s means no actions for state s)
|
|
protected List<List<Object>> actions;
|
|
|
|
// Flag: allow duplicates in distribution sets?
|
|
protected boolean allowDupes = false;
|
|
|
|
// Other statistics
|
|
protected int numDistrs;
|
|
protected int numTransitions;
|
|
protected int maxNumDistrs;
|
|
protected boolean maxNumDistrsOk;
|
|
|
|
// Constructors
|
|
|
|
/**
|
|
* Constructor: empty MDP.
|
|
*/
|
|
public MDPSimple()
|
|
{
|
|
initialise(0);
|
|
}
|
|
|
|
/**
|
|
* Constructor: new MDP with fixed number of states.
|
|
*/
|
|
public MDPSimple(int numStates)
|
|
{
|
|
initialise(numStates);
|
|
}
|
|
|
|
/**
|
|
* Copy constructor.
|
|
*/
|
|
public MDPSimple(MDPSimple mdp)
|
|
{
|
|
this(mdp.numStates);
|
|
copyFrom(mdp);
|
|
// Copy storage directly to avoid worrying about duplicate distributions (and for efficiency)
|
|
for (int s = 0; s < numStates; s++) {
|
|
List<Distribution> distrs = trans.get(s);
|
|
for (Distribution distr : mdp.trans.get(s)) {
|
|
distrs.add(new Distribution(distr));
|
|
}
|
|
}
|
|
if (mdp.actions != null) {
|
|
actions = new ArrayList<List<Object>>(numStates);
|
|
for (int s = 0; s < numStates; s++)
|
|
actions.add(null);
|
|
for (int s = 0; s < numStates; s++) {
|
|
if (mdp.actions.get(s) != null) {
|
|
int n = mdp.trans.get(s).size();
|
|
List<Object> list = new ArrayList<Object>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
list.add(mdp.actions.get(s).get(i));
|
|
}
|
|
actions.set(s, list);
|
|
}
|
|
}
|
|
}
|
|
// Copy flags/stats too
|
|
allowDupes = mdp.allowDupes;
|
|
numDistrs = mdp.numDistrs;
|
|
numTransitions = mdp.numTransitions;
|
|
maxNumDistrs = mdp.maxNumDistrs;
|
|
maxNumDistrsOk = mdp.maxNumDistrsOk;
|
|
}
|
|
|
|
/**
|
|
* Constructor: new MDP copied from an existing DTMC.
|
|
*/
|
|
public MDPSimple(DTMCSimple dtmc)
|
|
{
|
|
this(dtmc.getNumStates());
|
|
copyFrom(dtmc);
|
|
for (int s = 0; s < numStates; s++) {
|
|
// Note: DTMCSimple has no actions so can ignore these
|
|
addChoice(s, new Distribution(dtmc.getTransitions(s)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct an MDP from an existing one and a state index permutation,
|
|
* i.e. in which state index i becomes index permut[i].
|
|
* Note: have to build new Distributions from scratch anyway to do this,
|
|
* so may as well provide this functionality as a constructor.
|
|
*/
|
|
public MDPSimple(MDPSimple mdp, int permut[])
|
|
{
|
|
this(mdp.numStates);
|
|
copyFrom(mdp, permut);
|
|
// Copy storage directly to avoid worrying about duplicate distributions (and for efficiency)
|
|
// (Since permut is a bijection, all structures and statistics are identical)
|
|
for (int s = 0; s < numStates; s++) {
|
|
List<Distribution> distrs = trans.get(permut[s]);
|
|
for (Distribution distr : mdp.trans.get(s)) {
|
|
distrs.add(new Distribution(distr, permut));
|
|
}
|
|
}
|
|
if (mdp.actions != null) {
|
|
actions = new ArrayList<List<Object>>(numStates);
|
|
for (int s = 0; s < numStates; s++)
|
|
actions.add(null);
|
|
for (int s = 0; s < numStates; s++) {
|
|
if (mdp.actions.get(s) != null) {
|
|
int n = mdp.trans.get(s).size();
|
|
List<Object> list = new ArrayList<Object>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
list.add(mdp.actions.get(s).get(i));
|
|
}
|
|
actions.set(permut[s], list);
|
|
}
|
|
}
|
|
}
|
|
// Copy flags/stats too
|
|
allowDupes = mdp.allowDupes;
|
|
numDistrs = mdp.numDistrs;
|
|
numTransitions = mdp.numTransitions;
|
|
maxNumDistrs = mdp.maxNumDistrs;
|
|
maxNumDistrsOk = mdp.maxNumDistrsOk;
|
|
}
|
|
|
|
/**
|
|
* Construct an MDPSimple object from an MDPSparse one.
|
|
*/
|
|
public MDPSimple(MDPSparse mdp)
|
|
{
|
|
this(mdp.numStates);
|
|
copyFrom(mdp);
|
|
// Copy storage directly to avoid worrying about duplicate distributions (and for efficiency)
|
|
for (int s = 0; s < numStates; s++) {
|
|
for (int c = 0; c < mdp.getNumChoices(s); c++) {
|
|
Distribution distr = new Distribution();
|
|
Iterator<Entry<Integer, Double>> it = mdp.getTransitionsIterator(s, c);
|
|
while (it.hasNext()) {
|
|
Entry<Integer, Double> entry = it.next();
|
|
distr.add(entry.getKey(), entry.getValue());
|
|
}
|
|
this.addChoice(s, distr);
|
|
}
|
|
}
|
|
|
|
if (mdp.actions != null) {
|
|
actions = new ArrayList<List<Object>>(numStates);
|
|
for (int s = 0; s < numStates; s++)
|
|
actions.add(null);
|
|
for (int s = 0; s < numStates; s++) {
|
|
int n = mdp.getNumChoices(s);
|
|
List<Object> list = new ArrayList<Object>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
list.add(mdp.getAction(s, i));
|
|
}
|
|
actions.set(s, list);
|
|
}
|
|
}
|
|
// Copy flags/stats too
|
|
allowDupes = false; // TODO check this
|
|
numDistrs = mdp.numDistrs;
|
|
numTransitions = mdp.numTransitions;
|
|
maxNumDistrs = mdp.maxNumDistrs;
|
|
maxNumDistrsOk = true; // TODO not sure
|
|
}
|
|
|
|
// Mutators (for ModelSimple)
|
|
|
|
@Override
|
|
public void initialise(int numStates)
|
|
{
|
|
super.initialise(numStates);
|
|
numDistrs = numTransitions = maxNumDistrs = 0;
|
|
maxNumDistrsOk = true;
|
|
trans = new ArrayList<List<Distribution>>(numStates);
|
|
for (int i = 0; i < numStates; i++) {
|
|
trans.add(new ArrayList<Distribution>());
|
|
}
|
|
actions = null;
|
|
}
|
|
|
|
@Override
|
|
public void clearState(int s)
|
|
{
|
|
// Do nothing if state does not exist
|
|
if (s >= numStates || s < 0)
|
|
return;
|
|
// Clear data structures and update stats
|
|
List<Distribution> list = trans.get(s);
|
|
numDistrs -= list.size();
|
|
for (Distribution distr : list) {
|
|
numTransitions -= distr.size();
|
|
}
|
|
maxNumDistrsOk = false;
|
|
trans.get(s).clear();
|
|
if (actions != null && actions.get(s) != null)
|
|
actions.get(s).clear();
|
|
}
|
|
|
|
@Override
|
|
public int addState()
|
|
{
|
|
addStates(1);
|
|
return numStates - 1;
|
|
}
|
|
|
|
@Override
|
|
public void addStates(int numToAdd)
|
|
{
|
|
for (int i = 0; i < numToAdd; i++) {
|
|
trans.add(new ArrayList<Distribution>());
|
|
if (actions != null)
|
|
actions.add(null);
|
|
numStates++;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void buildFromPrismExplicit(String filename) throws PrismException
|
|
{
|
|
// we want to accurately store the model as it appears
|
|
// in the file, so we allow dupes
|
|
allowDupes = true;
|
|
|
|
int lineNum = 0;
|
|
// Open file for reading, automatic close
|
|
try (BufferedReader in = new BufferedReader(new FileReader(new File(filename)))) {
|
|
// Parse first line to get num states
|
|
String info = in.readLine();
|
|
lineNum = 1;
|
|
if (info == null) {
|
|
throw new PrismException("Missing first line of .tra file");
|
|
}
|
|
String[] infos = info.split(" ");
|
|
if (infos.length < 3) {
|
|
throw new PrismException("First line of .tra file must read #states, #choices, #transitions");
|
|
}
|
|
int n = Integer.parseInt(infos[0]);
|
|
int expectedNumChoices = Integer.parseInt(infos[1]);
|
|
int expectedNumTransitions = Integer.parseInt(infos[2]);
|
|
|
|
int emptyDistributions = 0;
|
|
|
|
// Initialise
|
|
initialise(n);
|
|
// Go though list of transitions in file
|
|
String s = in.readLine();
|
|
lineNum++;
|
|
while (s != null) {
|
|
s = s.trim();
|
|
if (s.length() > 0) {
|
|
String[] transition = s.split(" ");
|
|
int source = Integer.parseInt(transition[0]);
|
|
int choice = Integer.parseInt(transition[1]);
|
|
int target = Integer.parseInt(transition[2]);
|
|
double prob = Double.parseDouble(transition[3]);
|
|
|
|
if (source < 0 || source >= numStates) {
|
|
throw new PrismException("Problem in .tra file (line " + lineNum + "): illegal source state index " + source);
|
|
}
|
|
if (target < 0 || target >= numStates) {
|
|
throw new PrismException("Problem in .tra file (line " + lineNum + "): illegal target state index " + target);
|
|
}
|
|
|
|
// ensure distributions for all choices up to choice (inclusive) exist
|
|
// this potentially creates empty distributions that are never defined
|
|
// so we keep track of the number of distributions that are still empty
|
|
// and provide an error message if there are still empty distributions
|
|
// after having read the full .tra file
|
|
while (choice >= getNumChoices(source)) {
|
|
addChoice(source, new Distribution());
|
|
emptyDistributions++;
|
|
}
|
|
|
|
if (trans.get(source).get(choice).isEmpty()) {
|
|
// was empty distribution, becomes non-empty below
|
|
emptyDistributions--;
|
|
}
|
|
// add transition
|
|
if (! trans.get(source).get(choice).add(target, prob)) {
|
|
numTransitions++;
|
|
} else {
|
|
throw new PrismException("Problem in .tra file (line " + lineNum + "): redefinition of probability for " + source + " " + choice + " " + target);
|
|
}
|
|
|
|
// add action
|
|
if (transition.length > 4) {
|
|
String action = transition[4];
|
|
Object oldAction = getAction(source, choice);
|
|
if (oldAction != null && !action.equals(oldAction)) {
|
|
throw new PrismException("Problem in .tra file (line " + lineNum + "):"
|
|
+ "inconsistent action label for " + source + ", " + choice + ": "
|
|
+ oldAction + " and " + action);
|
|
}
|
|
setAction(source, choice, action);
|
|
}
|
|
}
|
|
s = in.readLine();
|
|
lineNum++;
|
|
}
|
|
// check integrity
|
|
if (getNumChoices() != expectedNumChoices) {
|
|
throw new PrismException("Problem in .tra file: unexpected number of choices: " + getNumChoices());
|
|
}
|
|
if (getNumTransitions() != expectedNumTransitions) {
|
|
throw new PrismException("Problem in .tra file: unexpected number of transitions: " + getNumTransitions());
|
|
}
|
|
assert(emptyDistributions >= 0);
|
|
if (emptyDistributions > 0) {
|
|
throw new PrismException("Problem in .tra file: there are " + emptyDistributions + " empty distribution, are there gaps in the choice indices?");
|
|
}
|
|
} catch (IOException e) {
|
|
throw new PrismException("File I/O error reading from \"" + filename + "\": " + e.getMessage());
|
|
} catch (NumberFormatException e) {
|
|
throw new PrismException("Problem in .tra file (line " + lineNum + ") for " + getModelType());
|
|
}
|
|
}
|
|
|
|
// Mutators (other)
|
|
|
|
/**
|
|
* Add a choice (distribution {@code distr}) to state {@code s} (which must exist).
|
|
* Distribution is only actually added if it does not already exists for state {@code s}.
|
|
* (Assuming {@code allowDupes} flag is not enabled.)
|
|
* Returns the index of the (existing or newly added) distribution.
|
|
* Returns -1 in case of error.
|
|
*/
|
|
public int addChoice(int s, Distribution distr)
|
|
{
|
|
List<Distribution> set;
|
|
// Check state exists
|
|
if (s >= numStates || s < 0)
|
|
return -1;
|
|
// Add distribution (if new)
|
|
if (!allowDupes) {
|
|
int i = indexOfChoice(s, distr);
|
|
if (i != -1)
|
|
return i;
|
|
}
|
|
set = trans.get(s);
|
|
set.add(distr);
|
|
// Add null action if necessary
|
|
if (actions != null && actions.get(s) != null)
|
|
actions.get(s).add(null);
|
|
// Update stats
|
|
numDistrs++;
|
|
maxNumDistrs = Math.max(maxNumDistrs, set.size());
|
|
numTransitions += distr.size();
|
|
return set.size() - 1;
|
|
}
|
|
|
|
/**
|
|
* Add a choice (distribution {@code distr}) labelled with {@code action} to state {@code s} (which must exist).
|
|
* Action/distribution is only actually added if it does not already exists for state {@code s}.
|
|
* (Assuming {@code allowDupes} flag is not enabled.)
|
|
* Returns the index of the (existing or newly added) distribution.
|
|
* Returns -1 in case of error.
|
|
*/
|
|
public int addActionLabelledChoice(int s, Distribution distr, Object action)
|
|
{
|
|
List<Distribution> set;
|
|
// Check state exists
|
|
if (s >= numStates || s < 0)
|
|
return -1;
|
|
// Add distribution/action (if new)
|
|
if (!allowDupes) {
|
|
int i = indexOfActionLabelledChoice(s, distr, action);
|
|
if (i != -1)
|
|
return i;
|
|
}
|
|
set = trans.get(s);
|
|
set.add(distr);
|
|
// Add null action if necessary
|
|
if (actions != null && actions.get(s) != null)
|
|
actions.get(s).add(null);
|
|
// Set action
|
|
setAction(s, set.size() - 1, action);
|
|
// Update stats
|
|
numDistrs++;
|
|
maxNumDistrs = Math.max(maxNumDistrs, set.size());
|
|
numTransitions += distr.size();
|
|
return set.size() - 1;
|
|
}
|
|
|
|
/**
|
|
* Set the action label for choice i in some state s.
|
|
* This method does not know about duplicates (i.e. if setting an action causes
|
|
* two choices to be identical, one will not be removed).
|
|
* Use {@link #addActionLabelledChoice(int, Distribution, Object)} which is more reliable.
|
|
*/
|
|
public void setAction(int s, int i, Object o)
|
|
{
|
|
// If action to be set is null, nothing to do
|
|
if (o == null)
|
|
return;
|
|
// If no actions array created yet, create it
|
|
if (actions == null) {
|
|
actions = new ArrayList<List<Object>>(numStates);
|
|
for (int j = 0; j < numStates; j++)
|
|
actions.add(null);
|
|
}
|
|
// If no actions for state i yet, create list
|
|
if (actions.get(s) == null) {
|
|
int n = trans.get(s).size();
|
|
List<Object> list = new ArrayList<Object>(n);
|
|
for (int j = 0; j < n; j++) {
|
|
list.add(null);
|
|
}
|
|
actions.set(s, list);
|
|
}
|
|
// Set actions
|
|
actions.get(s).set(i, o);
|
|
}
|
|
|
|
// Accessors (for Model)
|
|
|
|
@Override
|
|
public int getNumTransitions()
|
|
{
|
|
return numTransitions;
|
|
}
|
|
|
|
@Override
|
|
public void findDeadlocks(boolean fix) throws PrismException
|
|
{
|
|
for (int i = 0; i < numStates; i++) {
|
|
// Note that no distributions is a deadlock, not an empty distribution
|
|
if (trans.get(i).isEmpty()) {
|
|
addDeadlockState(i);
|
|
if (fix) {
|
|
Distribution distr = new Distribution();
|
|
distr.add(i, 1.0);
|
|
addChoice(i, distr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void checkForDeadlocks(BitSet except) throws PrismException
|
|
{
|
|
for (int i = 0; i < numStates; i++) {
|
|
if (trans.get(i).isEmpty() && (except == null || !except.get(i)))
|
|
throw new PrismException("MDP has a deadlock in state " + i);
|
|
}
|
|
}
|
|
|
|
// Accessors (for NondetModel)
|
|
|
|
@Override
|
|
public int getNumChoices(int s)
|
|
{
|
|
return trans.get(s).size();
|
|
}
|
|
|
|
@Override
|
|
public int getMaxNumChoices()
|
|
{
|
|
// Recompute if necessary
|
|
if (!maxNumDistrsOk) {
|
|
maxNumDistrs = 0;
|
|
for (int s = 0; s < numStates; s++)
|
|
maxNumDistrs = Math.max(maxNumDistrs, getNumChoices(s));
|
|
}
|
|
return maxNumDistrs;
|
|
}
|
|
|
|
@Override
|
|
public int getNumChoices()
|
|
{
|
|
return numDistrs;
|
|
}
|
|
|
|
@Override
|
|
public Object getAction(int s, int i)
|
|
{
|
|
List<Object> list;
|
|
if (i < 0 || actions == null || (list = actions.get(s)) == null)
|
|
return null;
|
|
return list.get(i);
|
|
}
|
|
|
|
@Override
|
|
public boolean allSuccessorsInSet(int s, int i, BitSet set)
|
|
{
|
|
return trans.get(s).get(i).isSubsetOf(set);
|
|
}
|
|
|
|
@Override
|
|
public boolean someSuccessorsInSet(int s, int i, BitSet set)
|
|
{
|
|
return trans.get(s).get(i).containsOneOf(set);
|
|
}
|
|
|
|
@Override
|
|
public Iterator<Integer> getSuccessorsIterator(final int s, final int i)
|
|
{
|
|
return trans.get(s).get(i).getSupport().iterator();
|
|
}
|
|
|
|
@Override
|
|
public SuccessorsIterator getSuccessors(final int s, final int i)
|
|
{
|
|
return SuccessorsIterator.from(getSuccessorsIterator(s, i), true);
|
|
}
|
|
|
|
// Accessors (for MDP)
|
|
|
|
@Override
|
|
public int getNumTransitions(int s, int i)
|
|
{
|
|
return trans.get(s).get(i).size();
|
|
}
|
|
|
|
@Override
|
|
public Iterator<Entry<Integer, Double>> getTransitionsIterator(int s, int i)
|
|
{
|
|
return trans.get(s).get(i).iterator();
|
|
}
|
|
|
|
|
|
|
|
// Accessors (other)
|
|
|
|
/**
|
|
* Get the list of choices (distributions) for state s.
|
|
*/
|
|
public List<Distribution> getChoices(int s)
|
|
{
|
|
return trans.get(s);
|
|
}
|
|
|
|
/**
|
|
* Get the ith choice (distribution) for state s.
|
|
*/
|
|
public Distribution getChoice(int s, int i)
|
|
{
|
|
return trans.get(s).get(i);
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the choice {@code distr} for state {@code s}, if it exists.
|
|
* If none, -1 is returned. If there are multiple (i.e. allowDupes is true), the first is returned.
|
|
*/
|
|
public int indexOfChoice(int s, Distribution distr)
|
|
{
|
|
return trans.get(s).indexOf(distr);
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the {@code action}-labelled choice {@code distr} for state {@code s}, if it exists.
|
|
* If none, -1 is returned. If there are multiple (i.e. allowDupes is true), the first is returned.
|
|
*/
|
|
public int indexOfActionLabelledChoice(int s, Distribution distr, Object action)
|
|
{
|
|
List<Distribution> set = trans.get(s);
|
|
int i, n = set.size();
|
|
if (distr == null) {
|
|
for (i = 0; i < n; i++) {
|
|
if (set.get(i) == null) {
|
|
Object a = getAction(s, i);
|
|
if (action == null) {
|
|
if (a == null)
|
|
return i;
|
|
} else {
|
|
if (action.equals(a))
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for (i = 0; i < n; i++) {
|
|
if (distr.equals(set.get(i))) {
|
|
Object a = getAction(s, i);
|
|
if (action == null) {
|
|
if (a == null)
|
|
return i;
|
|
} else {
|
|
if (action.equals(a))
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Standard methods
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
int i, j, n;
|
|
Object o;
|
|
String s = "";
|
|
s = "[ ";
|
|
for (i = 0; i < numStates; i++) {
|
|
if (i > 0)
|
|
s += ", ";
|
|
s += i + ": ";
|
|
s += "[";
|
|
n = getNumChoices(i);
|
|
for (j = 0; j < n; j++) {
|
|
if (j > 0)
|
|
s += ",";
|
|
o = getAction(i, j);
|
|
if (o != null)
|
|
s += o + ":";
|
|
s += trans.get(i).get(j);
|
|
}
|
|
s += "]";
|
|
}
|
|
s += " ]\n";
|
|
return s;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o)
|
|
{
|
|
if (o == null || !(o instanceof MDPSimple))
|
|
return false;
|
|
MDPSimple mdp = (MDPSimple) o;
|
|
if (numStates != mdp.numStates)
|
|
return false;
|
|
if (!initialStates.equals(mdp.initialStates))
|
|
return false;
|
|
if (!trans.equals(mdp.trans))
|
|
return false;
|
|
// TODO: compare actions (complicated: null = null,null,null,...)
|
|
return true;
|
|
}
|
|
}
|