001/* Linear state space model in the CT domain.
002
003 Copyright (c) 1998-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.domains.continuous.lib;
029
030import java.util.Iterator;
031
032import ptolemy.actor.Actor;
033import ptolemy.actor.Director;
034import ptolemy.actor.IORelation;
035import ptolemy.actor.TypedCompositeActor;
036import ptolemy.actor.TypedIOPort;
037import ptolemy.actor.TypedIORelation;
038import ptolemy.actor.lib.AddSubtract;
039import ptolemy.actor.lib.Scale;
040import ptolemy.data.DoubleMatrixToken;
041import ptolemy.data.expr.Parameter;
042import ptolemy.data.type.BaseType;
043import ptolemy.kernel.CompositeEntity;
044import ptolemy.kernel.util.Attribute;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.InternalErrorException;
047import ptolemy.kernel.util.NameDuplicationException;
048import ptolemy.kernel.util.Workspace;
049
050///////////////////////////////////////////////////////////////////
051//// LinearStateSpace
052
053/**
054 Linear state space model in the CT domain.
055
056 <p>The State-Space model implements a system whose behavior is defined by:
057 <pre>
058 dx/dt = Ax + Bu
059 y = Cx + Du
060 x(0) = x0
061 </pre>
062 where x is the state vector, u is the input vector, and y is the output
063 vector. The matrix coefficients must have the following characteristics:
064 <pre>
065 A must be an n-by-n matrix, where n is the number of states.
066 B must be an n-by-m matrix, where m is the number of inputs.
067 C must be an r-by-n matrix, where r is the number of outputs.
068 D must be an r-by-m matrix.
069 </pre>
070 The actor accepts <i>m</i> inputs and generates <i>r</i> outputs
071 through a multi-input port and a multi-output port. The widths of the
072 ports must match the number of rows and columns in corresponding
073 matrices, otherwise, an exception will be thrown.
074 <P>
075 This actor works like a higher-order function. It is opaque after
076 construction or the change of parameters. Upon preinitialization,
077 the actor will create a subsystem using integrators, adders, and
078 scales. After that, the actor becomes transparent, and the director
079 takes over the control of the actors contained by this actor.
080 <P>
081 This actor is based on the
082 ptolemy.domains.ct.lib.LinearStateSpace actor by Jie Liu.
083
084 @author Edward A. Lee, Contributor: Jie Liu
085 @version $Id$
086 @since Ptolemy II 8.0
087 @Pt.ProposedRating Red (liuj)
088 @Pt.AcceptedRating Red (cxh)
089 */
090public class LinearStateSpace extends TypedCompositeActor {
091    /** Construct the composite actor with a name and a container.
092     *  This constructor creates the ports, parameters, and the icon.
093     * @param container The container.
094     * @param name The name.
095     * @exception NameDuplicationException If another entity already had
096     * this name.
097     * @exception IllegalActionException If there was an internal problem.
098     */
099    public LinearStateSpace(CompositeEntity container, String name)
100            throws NameDuplicationException, IllegalActionException {
101        super(container, name);
102        _init();
103    }
104
105    /** Construct a LinearStateSpace in the specified
106     *  workspace with no container and an empty string as a name. You
107     *  can then change the name with setName(). If the workspace
108     *  argument is null, then use the default workspace.
109     *  @param workspace The workspace that will list the actor.
110     *  @exception IllegalActionException If the name has a period in it, or
111     *   the director is not compatible with the specified container.
112     *  @exception NameDuplicationException If the container already contains
113     *   an entity with the specified name.
114     */
115    public LinearStateSpace(Workspace workspace)
116            throws IllegalActionException, NameDuplicationException {
117        super(workspace);
118        _init();
119    }
120
121    ///////////////////////////////////////////////////////////////////
122    ////                        ports and parameters                 ////
123
124    /** Multi-input port.
125     */
126    public TypedIOPort input;
127
128    /** Multi-output port.
129     */
130    public TypedIOPort output;
131
132    /** State output multiport.
133     */
134    public TypedIOPort stateOutput;
135
136    /** The A matrix in the state-space representation. It must be a
137     *  square matrix.
138     *  The default value is [[1.0]].
139     */
140    public Parameter A;
141
142    /** The B matrix in the state-space representation. The number of
143     *  rows must equal to the number of rows of the A matrix. The number
144     *  of columns must equal to the width of the input port.
145     *  The default value is [[1.0]].
146     */
147    public Parameter B;
148
149    /** The C matrix in the state-space representation. The number of
150     *  columns must equal to the number of columns of the A matrix.
151     *  The number of rows must equal to the width of the output port.
152     *  The default value is [[1.0]].
153     */
154    public Parameter C;
155
156    /** The D matrix in the state-space representation. The number of
157     *  columns must equal to the width of the input port.
158     *  The number of rows must equal to the width of the output port.
159     *  The default value is [[0.0]].
160     */
161    public Parameter D;
162
163    /** The initial condition for the state variables. This must be
164     *  a vector (double matrix with only one row) whose length equals
165     *  to the number of state variables.
166     *  The default value is [0.0].
167     */
168    public Parameter initialStates;
169
170    ///////////////////////////////////////////////////////////////////
171    ////                         public methods                    ////
172
173    /** If the argument is <i>A, B, C, D</i> or <i>initialState</i>
174     *  parameters, check that they are indeed matrices and vectors,
175     *  and request for initialization from the director if there is one.
176     *  Other sanity checks like the dimensions of the matrices will
177     *  be done in the preinitialize() method.
178     *  @param attribute The attribute that changed.
179     *  @exception IllegalActionException If the numerator and the
180     *   denominator matrix is not a row vector.
181     */
182    @Override
183    public void attributeChanged(Attribute attribute)
184            throws IllegalActionException {
185        if (attribute == A) {
186            // Check that it is a square matrix.
187            DoubleMatrixToken token = (DoubleMatrixToken) A.getToken();
188
189            if (token.getRowCount() == 0 || token.getColumnCount() == 0
190                    || token.getRowCount() != token.getColumnCount()) {
191                throw new IllegalActionException(this,
192                        "The A matrix must be a nonempty square matrix.");
193            }
194
195            _requestInitialization = true;
196        } else if (attribute == B) {
197            // Check that B is a matrix.
198            DoubleMatrixToken token = (DoubleMatrixToken) B.getToken();
199
200            if (token.getRowCount() == 0 || token.getColumnCount() == 0) {
201                throw new IllegalActionException(this,
202                        "The B matrix must be a nonempty matrix.");
203            }
204
205            _requestInitialization = true;
206        } else if (attribute == C) {
207            // Check that C is a matrix.
208            DoubleMatrixToken token = (DoubleMatrixToken) C.getToken();
209
210            if (token.getRowCount() == 0 || token.getColumnCount() == 0) {
211                throw new IllegalActionException(this,
212                        "The C matrix must be a nonempty matrix.");
213            }
214
215            _requestInitialization = true;
216        } else if (attribute == D) {
217            DoubleMatrixToken token = (DoubleMatrixToken) D.getToken();
218
219            if (token.getRowCount() == 0 || token.getColumnCount() == 0) {
220                throw new IllegalActionException(this,
221                        "The D matrix must be a nonempty matrix.");
222            }
223
224            _requestInitialization = true;
225        } else if (attribute == initialStates) {
226            // The initialStates parameter should be a row vector.
227            DoubleMatrixToken token = (DoubleMatrixToken) initialStates
228                    .getToken();
229
230            if (token.getRowCount() != 1 || token.getColumnCount() < 1) {
231                throw new IllegalActionException(this,
232                        "The initialStates must be a row vector.");
233            }
234
235            // Changes of the initialStates parameter are ignored after
236            // the execution.
237        } else {
238            super.attributeChanged(attribute);
239        }
240    }
241
242    /** Return the executive director, regardless what isOpaque() returns.
243     *  //FIXME: this is not what this method does!!!
244     *  @return the executive director.
245     */
246    @Override
247    public Director getDirector() {
248        if (_opaque) {
249            return null;
250        } else {
251            return getExecutiveDirector();
252        }
253    }
254
255    /** Return the opaqueness of this composite actor. This actor is
256     *  opaque if it has not been preinitialized after creation or
257     *  changes of parameters. Otherwise, it is not opaque.
258     */
259    @Override
260    public boolean isOpaque() {
261        return _opaque;
262    }
263
264    /** Request the reinitialization.
265     *  @return True if the super class returns true.
266     *  @exception IllegalActionException If thrown by super class.
267     */
268    @Override
269    public boolean postfire() throws IllegalActionException {
270        // FIXME: the parameter change does affect the workspace version.
271        if (_requestInitialization) {
272            _requestInitialization();
273        }
274
275        return super.postfire();
276    }
277
278    /** Sanity check the parameters; if the parameters are legal
279     *  create a continuous-time subsystem that implement the model,
280     *  preinitialize all the actors in the subsystem,
281     *  and set the opaqueness of this actor to true.
282     *  This method needs the write access on the workspace.
283     *  @exception IllegalActionException If there is no CTDirector,
284     *  or any contained actors throw it in its preinitialize() method.
285     */
286    @Override
287    public void preinitialize() throws IllegalActionException {
288        // Check parameters.
289        _checkParameters();
290
291        // We work at the token level with out copying the matrices.
292        DoubleMatrixToken a = (DoubleMatrixToken) A.getToken();
293        int n = a.getRowCount();
294        DoubleMatrixToken b = (DoubleMatrixToken) B.getToken();
295        int m = b.getColumnCount();
296        DoubleMatrixToken c = (DoubleMatrixToken) C.getToken();
297        int r = c.getRowCount();
298
299        DoubleMatrixToken d = (DoubleMatrixToken) D.getToken();
300
301        /* DoubleMatrixToken x0 = (DoubleMatrixToken)*/initialStates.getToken();
302
303        try {
304            _workspace.getWriteAccess();
305
306            // remove the existing model
307            removeAllEntities();
308            removeAllRelations();
309
310            // Create the model
311            Integrator[] integrators = new Integrator[n];
312            IORelation[] states = new IORelation[n];
313
314            AddSubtract[] stateAdders = new AddSubtract[n];
315
316            // Integrators
317            for (int i = 0; i < n; i++) {
318                integrators[i] = new Integrator(this, "state_" + i);
319                integrators[i].initialState
320                        .setExpression("initialStates(0," + i + ")");
321                states[i] = new TypedIORelation(this, "relation_state_" + i);
322                integrators[i].state.link(states[i]);
323
324                // One adder per integrator.
325                stateAdders[i] = new AddSubtract(this, "stateAdder_" + i);
326                connect(stateAdders[i].output, integrators[i].derivative);
327                stateOutput.link(states[i]);
328            }
329
330            // State feedback
331            Scale[][] feedback = new Scale[n][n];
332
333            for (int i = 0; i < n; i++) {
334                for (int j = 0; j < n; j++) {
335                    // We don't create the Scale if the corresponding element
336                    // in the A matrix is 0.
337                    if (a.getElementAt(i, j) == 0.0) {
338                        continue;
339                    }
340                    feedback[i][j] = new Scale(this, "feedback_" + i + "_" + j);
341                    feedback[i][j].factor
342                            .setExpression("A(" + i + ", " + j + ")");
343                    feedback[i][j].input.link(states[j]);
344                    connect(feedback[i][j].output, stateAdders[i].plus);
345                }
346            }
347
348            // Inputs
349            Scale[][] inputScales = new Scale[n][m];
350            IORelation[] inputs = new IORelation[m];
351
352            for (int j = 0; j < m; j++) {
353                inputs[j] = new TypedIORelation(this, "relation_input_" + j);
354                input.link(inputs[j]);
355
356                // Create input scales.
357                for (int i = 0; i < n; i++) {
358                    // We create a scale for each input even if the
359                    // corresponding element in B is 0. Otherwise,
360                    // if the elements of A's in this row are also zero,
361                    // then we will have an illegal topology.
362                    inputScales[i][j] = new Scale(this, "b_" + i + "_" + j);
363                    inputScales[i][j].factor
364                            .setExpression("B(" + i + ", " + j + ")");
365                    inputScales[i][j].input.link(inputs[j]);
366                    connect(inputScales[i][j].output, stateAdders[i].plus);
367                }
368            }
369
370            // Outputs
371            AddSubtract[] outputAdders = new AddSubtract[r];
372            Scale[][] outputScales = new Scale[r][n];
373
374            for (int l = 0; l < r; l++) {
375                outputAdders[l] = new AddSubtract(this, "outputAdder" + l);
376                connect(outputAdders[l].output, output);
377
378                for (int i = 0; i < n; i++) {
379                    // Create the output scales only if the corresponding
380                    // 'c' element is not 0.
381                    if (c.getElementAt(l, i) == 0.0) {
382                        continue;
383                    }
384                    outputScales[l][i] = new Scale(this,
385                            "outputScale_" + l + "_" + i);
386                    outputScales[l][i].factor
387                            .setExpression("C(" + l + ", " + i + ")");
388                    outputScales[l][i].input.link(states[i]);
389                    connect(outputScales[l][i].output, outputAdders[l].plus);
390                }
391            }
392
393            // Direct feed through.
394            Scale[][] feedThrough = new Scale[r][m];
395
396            for (int l = 0; l < r; l++) {
397                for (int j = 0; j < m; j++) {
398                    // Create the scale only if the element is not 0:
399                    if (d.getElementAt(l, j) == 0.0) {
400                        continue;
401                    }
402                    feedThrough[l][j] = new Scale(this,
403                            "feedThrough_" + l + "_" + j);
404                    feedThrough[l][j].factor
405                            .setExpression("D(" + l + ", " + j + ")");
406                    feedThrough[l][j].input.link(inputs[j]);
407                    connect(feedThrough[l][j].output, outputAdders[l].plus);
408                }
409            }
410
411            _opaque = false;
412            _workspace.incrVersion();
413        } catch (NameDuplicationException ex) {
414            // Should never happen.
415            throw new InternalErrorException("Duplicated name when "
416                    + "constructing the subsystem" + ex.getMessage());
417        } finally {
418            _workspace.doneWriting();
419        }
420
421        // NOTE: Cannot call super.preinitialize() because the actor is not
422        // opaque.
423        // super.preinitialize();
424
425        // preinitialize all contained actors.
426        for (Iterator i = deepEntityList().iterator(); i.hasNext();) {
427            Actor actor = (Actor) i.next();
428            actor.preinitialize();
429        }
430    }
431
432    /** Stop the current firing. This method overrides the stopFire()
433     *  method in TypedCompositeActor base class, so that it will not
434     *  invoke the local director (since there is none). This method
435     *  should not be called after initialization phase, i.e. when
436     *  the actor is transparent.
437     */
438    @Override
439    public void stopFire() {
440        return;
441    }
442
443    /** Set the opaqueness back to true and call the wrapup() method
444     *  of the super class.
445     *  @exception IllegalActionException If there is no director.
446     */
447    @Override
448    public void wrapup() throws IllegalActionException {
449        _opaque = true;
450        super.wrapup();
451    }
452
453    ///////////////////////////////////////////////////////////////////
454    ////                         private methods                   ////
455
456    /** Check the dimensions of all parameters and ports.
457     *  @exception IllegalActionException If the dimensions are illegal.
458     */
459    private void _checkParameters() throws IllegalActionException {
460        DoubleMatrixToken a = (DoubleMatrixToken) A.getToken();
461        int n = a.getRowCount();
462        DoubleMatrixToken b = (DoubleMatrixToken) B.getToken();
463
464        if (b.getRowCount() != n) {
465            throw new IllegalActionException(this,
466                    "The number of rows of the B matrix (" + b.getRowCount()
467                            + ") should be equal to "
468                            + "the number of rows of the A matrix (" + n
469                            + ").");
470        }
471
472        int m = b.getColumnCount();
473
474        if (input.getWidth() != m) {
475            throw new IllegalActionException(this,
476                    "The number of columns of the B matrix ("
477                            + b.getColumnCount() + ") should be equal to "
478                            + "the width of the input port (" + input.getWidth()
479                            + ").");
480        }
481
482        DoubleMatrixToken c = (DoubleMatrixToken) C.getToken();
483
484        if (c.getColumnCount() != n) {
485            throw new IllegalActionException(this,
486                    "The number of columns of the C matrix ("
487                            + c.getColumnCount() + ") should be equal to "
488                            + "the number of rows of the A matrix (" + n
489                            + ").");
490        }
491
492        // The output width is not checked, since we may only want
493        // to use some of the outputs
494        DoubleMatrixToken d = (DoubleMatrixToken) D.getToken();
495
496        if (c.getRowCount() != d.getRowCount()) {
497            throw new IllegalActionException(this,
498                    "The number of rows of the D matrix (" + d.getRowCount()
499                            + ") should be equal to "
500                            + "the number of rows of the C matrix ("
501                            + c.getRowCount() + ").");
502        }
503
504        if (d.getColumnCount() != input.getWidth()) {
505            throw new IllegalActionException(this,
506                    "The number of columns of the D matrix ("
507                            + d.getColumnCount() + ") should be equal to "
508                            + "the width of the input port (" + input.getWidth()
509                            + ").");
510        }
511
512        DoubleMatrixToken x0 = (DoubleMatrixToken) initialStates.getToken();
513
514        if (x0.getColumnCount() != n) {
515            throw new IllegalActionException(this,
516                    "The number of initial states (" + x0.getColumnCount()
517                            + ") should equal to "
518                            + "the number of columns of the A matrix (" + n
519                            + ").");
520        }
521    }
522
523    /** Initialize the class. */
524    private void _init()
525            throws IllegalActionException, NameDuplicationException {
526        input = new TypedIOPort(this, "input", true, false);
527        input.setMultiport(true);
528        output = new TypedIOPort(this, "output", false, true);
529        output.setMultiport(true);
530        stateOutput = new TypedIOPort(this, "stateOutput", false, true);
531        stateOutput.setMultiport(true);
532        _opaque = true;
533        _requestInitialization = true;
534
535        double[][] one = { { 1.0 } };
536        double[][] zero = { { 0.0 } };
537
538        A = new Parameter(this, "A", new DoubleMatrixToken(one));
539        A.setTypeEquals(BaseType.DOUBLE_MATRIX);
540
541        B = new Parameter(this, "B", new DoubleMatrixToken(one));
542        B.setTypeEquals(BaseType.DOUBLE_MATRIX);
543
544        C = new Parameter(this, "C", new DoubleMatrixToken(one));
545        C.setTypeEquals(BaseType.DOUBLE_MATRIX);
546
547        D = new Parameter(this, "D", new DoubleMatrixToken(zero));
548        D.setTypeEquals(BaseType.DOUBLE_MATRIX);
549
550        initialStates = new Parameter(this, "initialStates",
551                new DoubleMatrixToken(zero));
552        initialStates.setTypeEquals(BaseType.DOUBLE_MATRIX);
553        setClassName("ptolemy.domains.ct.lib.LinearStateSpace");
554
555        // icon
556        _attachText("_iconDescription", "<svg>\n" + "<rect x=\"-50\" y=\"-30\" "
557                + "width=\"100\" height=\"60\" " + "style=\"fill:white\"/>\n"
558                + "<text x=\"-45\" y=\"-10\" " + "style=\"font-size:14\">\n"
559                + "dx/dt=Ax+Bu " + "</text>\n" + "<text x=\"-45\" y=\"10\" "
560                + "style=\"font-size:14\">\n" + "    y=Cx+Du" + "</text>\n"
561                + "</svg>\n");
562    }
563
564    /** Set this composite actor to opaque and request for reinitialization
565     *  from the director if there is one.
566     */
567    private void _requestInitialization() {
568        // Request for initialization.
569        Director dir = getDirector();
570
571        if (dir != null) {
572            dir.requestInitialization(this);
573        }
574
575        // Set this composite to opaque.
576        _opaque = true;
577    }
578
579    ///////////////////////////////////////////////////////////////////
580    ////                         private variables                 ////
581    // opaqueness.
582    private boolean _opaque;
583
584    // request for initialization.
585    private boolean _requestInitialization;
586}