From 966a20f1dc6188616680215ed6cfb5376bea281d Mon Sep 17 00:00:00 2001 From: ch Date: Wed, 13 Apr 2022 14:00:51 +0200 Subject: [PATCH] emgr v5.99 --- CHANGELOG | 15 + CITATION | 12 +- CITATION.cff | 16 +- CODE | 6 +- LICENSE | 2 +- README.md | 38 ++- RUNME.m | 28 +- VERSION | 2 +- codemeta.json | 26 ++ emgr-ref.pdf | Bin 63924 -> 79499 bytes emgr.m | 812 ++++++++++++++++++++++++------------------------ emgrTest.m | 6 +- est.m | 124 ++++---- est.scm | 352 ++++++++++----------- estDemo.m | 11 +- estProbe.m | 6 +- estTest.m | 91 +++++- py/RUNME.py | 32 +- py/emgr.py | 721 ++++++++++++++++++++++-------------------- py/emgrProbe.py | 21 +- test_dwj.m | 101 ++++++ 21 files changed, 1359 insertions(+), 1063 deletions(-) create mode 100644 codemeta.json create mode 100644 test_dwj.m diff --git a/CHANGELOG b/CHANGELOG index cb9290d..146413c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,20 @@ # emgr Changelog +## emgr 5.99 (2022-04) + + * **CHANGED** parameter scale computation + * **ADDED** parameter centering around nominal parameter + * **ADDED** exact Schur complement for (cross-)identifiability Gramian + * **ADDED** inline-if local function for cleaner assignments + * **ADDED** Lie-bracket kernel and hyperbolic-SVD kernel for testing + * **ADDED** reciprocal square-root time-weighting + * **FIXED** normalization bug + * **FIXED** approximate inverse (Python) + * **IMPROVED** observability and adjoint caching + * **IMPROVED** default solver + * **IMPROVED** time-weighting + * **IMPROVED** code readability + ## emgr 5.9 (2021-01) * **CHANGED** chirp input function character changed to 'h' diff --git a/CITATION b/CITATION index dde9338..b5fbe07 100644 --- a/CITATION +++ b/CITATION @@ -1,14 +1,14 @@ Cite As: -C. Himpe (2021). emgr - EMpirical GRamian Framework (Version 5.9) [Software]. -Available from https://gramian.de . doi:10.5281/zenodo.4454679 +C. Himpe (2022). emgr - EMpirical GRamian Framework (Version 5.99) [Software]. +Available from https://gramian.de . doi:10.5281/zenodo.6457616 BibTeX Entry: -@MISC{emgr59, +@MISC{emgr599, author = {C.~Himpe}, - title = {{emgr - EMpirical GRamian Framework} (Version~5.9)}, + title = {{emgr - EMpirical GRamian Framework} (Version~5.99)}, howpublished = {\url{https://gramian.de}}, - year = {2021}, - doi = {10.5281/zenodo.4454679} + year = {2022}, + doi = {10.5281/zenodo.6457616} } diff --git a/CITATION.cff b/CITATION.cff index 0b52fac..0b6541c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,15 +1,19 @@ -cff-version: 1.1.0 +cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - family-names: Himpe given-names: Christian orcid: https://orcid.org/0000-0003-2194-6754 title: "emgr - EMpirical GRamian Framework" -version: 5.9 -date-released: 2021-01-21 -doi: 10.5281/zenodo.4454679 +version: 5.99 +date-released: 2022-04-13 +doi: 10.5281/zenodo.6457616 license: BSD-2-Clause url: https://gramian.de abstract: "Empirical system Gramians for (nonlinear) input-output systems." -keywords: Controllability, Observability, Empirical Gramians, Model Reduction, Model Order Reduction - +keywords: + - "Controllability" + - "Observability" + - "Empirical Gramians" + - "Model Reduction" + - "Model Order Reduction" diff --git a/CODE b/CODE index 46296a5..1c8d132 100644 --- a/CODE +++ b/CODE @@ -1,9 +1,9 @@ # code.ini name: Empirical Gramian Framework shortname: emgr -version: 5.9 -release-date: 2021-01-21 -id: 10.5281/zenodo.4454679 +version: 5.99 +release-date: 2022-04-13 +id: 10.5281/zenodo.6457616 id-type: doi author: Christian Himpe orcid: 0000-0003-2194-6754 diff --git a/LICENSE b/LICENSE index 5455662..f99a0f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ All source code is licensed under the open source BSD-2-Clause license: -Copyright (c) 2013--2021, Christian Himpe +Copyright (c) 2013--2022, Christian Himpe All rights reserved. diff --git a/README.md b/README.md index 830583f..bd8e7a4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ ![code meta-data](https://img.shields.io/badge/code_meta--data-%E2%9C%93-brightgreen.svg) -![zenodo listed](https://zenodo.org/badge/doi/10.5281/zenodo.4454679.png) +![zenodo listed](https://zenodo.org/badge/doi/10.5281/zenodo.6457616.png) ![matlab compatible](https://img.shields.io/badge/matlab-compatible-lightgrey.svg) ![SWH](https://archive.softwareheritage.org/badge/origin/https://github.com/gramian/emgr/) -![emgr logo](emgr.png) emgr -- EMpirical GRamian Framework (5.9) -================================================================ +![emgr logo](emgr.png) emgr -- EMpirical GRamian Framework (5.99) +================================================================= [Website](https://gramian.de) | [Twitter](https://twitter.com/modelreduction) | [Feedback](ch@gramian.de) -* [emgr - EMpirical GRamian Framework](https://gramian.de) -* version: **5.9** (2021-01-21) +* project: [emgr - EMpirical GRamian Framework](https://gramian.de) +* version: **5.99** (2022-04-13) * by: [Christian Himpe](https://orcid.org/0000-0003-2194-6754) * under: [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) License * summary: Empirical system Gramians for (nonlinear) input-output systems. @@ -25,7 +25,7 @@ - Combined State and Parameter Reduction * Decentralized Control * Sensitivity Analysis -* Parameter Identification | Structural Identifiability | Input-Output Identifiability +* Parameter Identification | Input-Output Identifiability * Nonlinearity Quantification * Uncertainty Quantification * System Norms | System Indices | System Invariants @@ -65,7 +65,7 @@ detailing most features and capabilities see: * C. Himpe. [emgr -- the Empirical Gramian Framework](https://doi.org/10.3390/a11070091). - Algorithms 11(7): 91, 2018. + Algorithms 11(7): 91, 2018. (open access) and references therein. See also the [reference lists](https://gramian.de/emgr-est.md) for further theoretical backgrounds on empirical Gramians. @@ -75,15 +75,15 @@ See also the [reference lists](https://gramian.de/emgr-est.md) for further theor Successfully tested on: * Mathworks MATLAB 2017b -* Mathworks MATLAB 2020b +* Mathworks MATLAB 2022a * GNU Octave 5.2.0 -* GNU Octave 6.1.0 -* Python 3.8.5 (NumPy 1.17.4) +* GNU Octave 7.1.0 +* Python 3.8.10 (NumPy 1.17.4) ## Citation -* C. Himpe (2021). emgr -- EMpirical GRamian Framework (Version 5.9) [Software]. - https://gramian.de [doi:10.5281/zenodo.4454679](https://doi.org/10.5281/zenodo.4454679) +* C. Himpe (2022). emgr -- EMpirical GRamian Framework (Version 5.99) [Software]. + https://gramian.de [doi:10.5281/zenodo.6457616](https://doi.org/10.5281/zenodo.6457616) ## Getting Started @@ -110,9 +110,13 @@ emgrDemo(id) % with id one of 'hnm', 'isp', 'fss', 'nrc', 'rqo', 'lte', 'aps', ' [`CHANGELOG`](CHANGELOG) Version Information -[`CITATION`](CITATION) Citation Information +[`CITATION`](CITATION) Citation Information (`BibTeX`) -[`CODE`](CODE) Meta Information +[`CITATION.cff`](CITATION.cff) Citation Information (`CFF`) + +[`CODE`](CODE) Meta Information (`INI`) + +[`codemeta.json`](coedmeta.json) Meta Information (`JSON`) [`LICENSE`](LICENSE) License Information @@ -120,11 +124,11 @@ emgrDemo(id) % with id one of 'hnm', 'isp', 'fss', 'nrc', 'rqo', 'lte', 'aps', ' [`RUNME.m`](RUNME.m) Minimal Code Example -[`emgr.m`](emgr.m) Empirical Gramian Framework (main file, crc32:`c136507c`) +[`emgr.m`](emgr.m) Empirical Gramian Framework (main file, crc32:`f76f4638`) [`emgrTest.m`](emgrTest.m) Run all tests -[`est.m`](est.m) Empirical System Theory (EST) emgr frontend +[`est.m`](est.m) Empirical System Theory (EST) emgr prototype frontend [`estTest.m`](estTest.m) EST System Tests @@ -142,6 +146,8 @@ emgrDemo(id) % with id one of 'hnm', 'isp', 'fss', 'nrc', 'rqo', 'lte', 'aps', ' [`estProbe.m`](estProbe.m) emgr factorial comparison of singular value decays +[`test_dwj.m`](test_dwj.m) Test partioned joint Gramian computation + [`est.scm`](est.scm) EST tree-based documentation in [Scheme](https://en.wikipedia.org/wiki/Scheme_(programming_language)) [`emgr-ref.pdf`](emgr-ref.pdf) emgr reference cheat sheet diff --git a/RUNME.m b/RUNME.m index 9887c6e..5a3f571 100644 --- a/RUNME.m +++ b/RUNME.m @@ -1,5 +1,5 @@ %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: RUNME - minimal test script @@ -7,19 +7,19 @@ emgr_VERSION = emgr('version') % Linear System -A = -eye(4) % System Matrix -B = [0;1;0;1] % Input Matrix -C = [0,0,1,1] % Output Matrix +A = -eye(4) % System Matrix +B = [0;1;0;1] % Input Matrix +C = [0,0,1,1] % Output Matrix -P = zeros(4,1); % Parameter Vector -Q = [zeros(4,1),[0.25;0.5;0.75;1.0]]; % Min and Max Parameter Box +P = zeros(4,1); % Parameter Vector +Q = [0.01*ones(4,1),[0.25;0.5;0.75;1.0]]; % Min and Max Parameter Box -f = @(x,u,p,t) A*x + B*u + p; % (Affine) Linear Vector Field -g = @(x,u,p,t) C*x; % Linear Output Functional -h = @(x,u,p,t) A'*x + C'*u; % Adjoint Vector Field +f = @(x,u,p,t) A*x + B*u + p; % (Affine) Linear Vector Field +g = @(x,u,p,t) C*x; % Linear Output Functional +h = @(x,u,p,t) A'*x + C'*u; % Adjoint Vector Field -s = [1,4,1]; % System dimension -t = [0.01,1.0]; % Time discretization +s = [1,4,1]; % System dimension +t = [0.01,1.0]; % Time discretization % (Empircal) Controllability Gramian WC = emgr(f,g,s,t,'c',P) @@ -34,11 +34,11 @@ WY = emgr(f,h,s,t,'y',P) % (Empirical) Sensitivity Gramian -WCWS = emgr(f,g,s,t,'s',Q); WS = WCWS{2} +WCWS = emgr(f,g,s,t,'s',Q) % (Empirical) Identifiability Gramian -WOWI = emgr(f,g,s,t,'i',Q); WI = WOWI{2} +WOWI = emgr(f,g,s,t,'i',Q) % (Empirical) Cross-Identifiability Gramian -WXWJ = emgr(f,g,s,t,'j',Q); WJ = WXWJ{2} +WXWJ = emgr(f,g,s,t,'j',Q) diff --git a/VERSION b/VERSION index 95ee81a..4c9949e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.9 +5.99 diff --git a/codemeta.json b/codemeta.json new file mode 100644 index 0000000..51f22a0 --- /dev/null +++ b/codemeta.json @@ -0,0 +1,26 @@ +{ + "@context": "10.5281/zenodo.6457616", + "@type": "SoftwareSourceCode", + "identifier": "emgr", + "name": "emgr -- EMpirical GRamian Framework", + "author": { "@type": "Person", "givenName": "Christian", "familyName": "Himpe", "@id": "http://orcid.org/0000-0003-2194-6754" }, + "description": "Empirical system Gramians for (nonlinear) input-output systems.", + "version": "5.99", + "datePublished": "2022-04-13", + "license": "https://opensource.org/licenses/BSD-2-Clause", + "copyrightHolder": "Christian Himpe", + "copyrightYear": "2013", + "programmingLanguage": "Matlab", + "runtimePlatform": [ "Mathworks MATLAB >=2017b", "GNU Octave >=5.2" ], + "url": "https://gramian.de", + "downloadUrl": "https://gramian.de/emgr.m", + "codeRepository": "https://github.com/gramian/emgr", + "readme": "https://gramian.de/README.md", + "releaseNotes": "https://gramian.de/CHANGELOG", + "citation": [ "https://gramian.de/CITATION", "https://gramian.de/CITATION.cff"], + "codemeta": [ "https://gramian.de/codemeta.json", "https://gramian.de/CODE" ], + "referencePublication": "https://doi.org/10.3390/a11070091", + "applicationCategory": "Science", + "applicationSubCategory": "Mathematics", + "keywords": [ "Controllability", "Observability", "Cross Gramian", "Model Reduction", "Model Order Reduction" ] +} diff --git a/emgr-ref.pdf b/emgr-ref.pdf index 7b4f82ff9e089e84e4ebef3db95de810001def89..0bd01f1c9f5d204d679158bd4e8ed95b6cede3e3 100644 GIT binary patch delta 49852 zcmV(zK<2-+^8<_R1dvRBj^sA7_x_4J8z>F;1uzPz^)NRB4a}i2w;2!c4g)-2-tWI) za1kXjNJ^R2whd%gWr<`k_6P<;Y~i>6`}V&#c4G?@?&cI}}I0JbhAgj)>X0hWPRc2U*<@NV(K+=O1VwVLA<-#PpS=wDSvh>rz6n#Y;gfl~Ex0;+ZS-I=!zn zesW*w>1+E+gPZ$*O52g%EK>SH(Kh>Jisq*goU@#o2hVZprKR%D^WePm&G@`?Cl{?} zwMKrLYqMx}o)&q0mloVFMydjlk&D+PK3RSk+(ogi2mUs$ys zef^V#73cjtj3Wlu4e(HhFM=uT_LGi*V+$R>|Y%v`T*Z zl2*xsdz>r)EX${W^Wv@JbN*i(^;b9=D1t}Fa6-znThbtXV%AKghc8))G?oLj&NyNr zBfzp1pag_UAs%hXjxvi?;(81ahi7Qo2u{$HXXWC5+z!FN-OAG#N5lW{5&ycf>B{>> zGHAY3Dmc=XUb_+-Tuk++120nqJ}2>{pPxh`0$|D%{5NPMBID~{xm_7?Pwn{VdxQ#q z{=^*xTgd4--a+$ztGX}IiM1i-4YudN+GfDo^|%d&Jbx*B%B`wgLHdA8XS(`1A^BpIX}Xx2v`eW zva%bZQr80;)}<&JkfhJm-k=1oY6$+A9vsp3ES!b56rB76m8p(4f1sx% z#Q%V;gPwXF=&5?#ITOr3@oUserq504G(N?eOXQV}+S zjX;YGgpK5*x7vD(?6sehv0~dv8t!cSz*|o;1Hp>r2Tvi`puWu1;~L&iB@qWf$tL}^ zxIC+M7a}8KBReChA)$GQ)UnlB}EV?0*Wk+MnC_R-$w;s1ayUB=h3MV;I?s#w%CgJ_WIVq*jW&j3G~? zDi_njNJ0|*OmDq6oqC- zgKUVDoley`=ZMx>wMi{>_I z%U#%l*w7{tH^!G2(d&mXWhZ!_pJu+VUO)*rDh8C_X7P1=kD@ zrSOiuYd>yCUka{~k3z#WLEmTyM_KJFK}w;V)C>29 zGHz$2u|3l`VA~9Tywe*Pl92#%ET$xo7*!_1yjBq=f-T;GFydoxcvE;1>4=Xfg8R&- zX{uGPn}d0KtON@Pgt(yiz@FfP_DtiDWHaz+^b(3Q2W0g;A~*rfSZ*n3(wW*zisQD> z$Vbz;e=07STSPv*m^_3QTs`b4Jh`>Z0;(E1WalLaHs{xW*3%KE3kP%e)h=y&ixZ&^ zwV9Y$K>YwP`=fLIb-WY2Ywge6xMMDo=UY2^v9`UGzx7kGC%H%Yni3+NJ>O$Jh458oAAysoDMjT6a| zhIxwxUfClZ%RRZ|L*%}Vk8ylKf^~0v!NITOG@XQtr=-;Q%$y|FY;_QGi4(Ve*Glg; z<*Nh8bnHVK*UX|C{$-7$$6RSRV9|Q`$voJ&9B5E~21G7?PZ?0*)iz?r5fzP+$3;}d z4pG!=+M#}ClPeF-5~u(LbGPcB@GNvXGR=oOo9Q-0`dr=iRi%o^8}V3k!#E%7IrHjY zRtCQvo!_g;3p=#2G7hZB{ofNq+2%? zZEK%@zXnkm3GqHE<5N)?3Hw;h?n+lH3i4IgCL)_Ng&M_plEm>bFFi7zs|8>y7wXXTXvzVHaV zz2mgOa>TtaW~eh;nCoMK3d9dt(zJGPM~gNhTcGb_$lEZKq)Q(#oaG8{CJKEp>3q>=bWoCOO!G%KJ-y z@O`dYtW`@<|2-7}Np<9(oyc4^`XmbWMt|-$3WSKMzH*svvAJdakJi8eR%S&p8Sl7q=33^ zAqhx^B~BD1J^6d6iqYW~aqDIopHeaP@JFbakpiLCyQo02m87t+w~~V^bJe`v@1;42AfU*6|NhQ?RaYXSe+1B5wuF29-|cM*-u#m60DzbVUYrVo2=_C z#H7GZmD>rC2-l$=WmmA?uiUQv*+W72PjiZdA?`e;)&c{KRS$nam2CeEHK;6V!8?7P zHX0^u9|g~2?`mq#qG}ZuxU*G|qGNf662yuAFZBb98olPTcQR!#t7KO7Zm^0^PWL}Ck0Qexb0WB zf3BwjwVthENMnE<_;F3|WhF{aBg0WzFPb!1`&XvAte`musXW8jtAlBo@sI zF)*f!{|XBdHoRjR9yG##ZGp;vVQ;&dOK`(?;ivTL%28Fuk*+vp`J|8UQnH7NP>4mW zQ=$8}SKwZ!3E@dhDA2~FQfgvgjN|3Ii-(6_O^lhhE#)?ByCf*%k}+*+)5` z3_r=Yge+=O>tiP{vMmyNPuq+9;-ehMO?B(Z!-6*^Sso_wLzM4-Sou!o?`ZXsPx*q+ zW|}us*3#rkcv4-=`E0PUd@5wD+q-NUv~6IaTvMTT zaV4Jp$~xf^e~$*-L?15%Ht8D&-is){&HmAs=w!fCsgn$UIu%&dNsFWh{pBbq16l}f zk>I7f-S%`fkY;d@jPU9Q;I{1L>y@7CLi~Olx8^zq-zEQ8Gd}HEg!R7*L{> z!9LM!WpfRG?<=F`Z7FHh*94>a>U!4h>H`%T>R|lj76~)H30@S(<{6tif2%^Lh$#&O zc$~uexa**FupnzV&8NuazbXWOG<{U)O=7pZo}!Nq*)ahFL(6G1?>kDvG5QRyv0+Dh9L@wzR!WHL^?Br5B@oXf@=2FoK@`tO?EeTJGPk|GYKA@4&x5S}VrYO# zkKW`KEi}No2XA-*i@yLa4B)H-FzL@*gN@xtfG_#;MfU_31%%)(-7`^s_m@#mDkOW{ zcHeV<=0V|WBP6h$_x?yfsx@Y@D{V}7^b?cuK0OR{WkHkPBG5^KFuzzAJJWbp3>n0u zm~EqFx$p#-&}NqHLNmSUO2ED>4+BR1to}q_K|+WeUbR(kxDL|!dp}5*I!47(h2zsR zJ{?5&CI~gT^2c+(xB_e!w%Z{Vrf4i}GA-?Yg~Qy8|gHeV}gxA(esrZPw;W* zz_WsSf{%f`_x6O0>C*bhb9cW*zW>$yUx)E^z4=otjoK!3^$Br$!%op|aIjO7UU(a*EkO<*NbqD29zV1#320>WaO*9d zk|+TY5#odcGoOiT_+a6IL@Bvg?90?%m!e^Ck%9yg>vkkchm@eFh(94xLZO757byY0 z6~7=+^@67)>NKQTq6kRIml0&ta0Y~b;}=i^5F0Np;HF;j6mCv~nsGycI?^_DtD!2% z7!!$6{7t1MszMa%bxAR{dM~M#&4v43i=?e88rDsX&P$|>FCZDVemWeQ(08bVCu#R{ zo=Km8?FB|_WrI&$zTTDn+!-&Di4~O)*o3*4SaAyvH!JIbbBPs$=flEFaH|@Bjyid~ z8{T~aT9^WrK6!+g#Cl7CI&t!7RD8E91u~hb01uGiK!PLg%2Ws^Lz?e6=^AKO^>`OW zwyx!CDU6LsQ>U6D1tt2+^JNmvn7ve;B z_oEc<8!iULnMEqdzEPNSKJ;6tIKGNzH*9YvIy*exv@sK@I_h_J#y*0|Gbh zE_N&ssryS&g{@4WTS{zq!t2phQtHq5dPsjqr1wQ{QfTASJ&S=qxy`<#6q~}eSL`@S zsZWvC^?O<89Hsa&`8KE>M@eFlD*@`9qhwJ)36Oh^l46o(qPrX=`l9*%o^+$1!~WtJ zc~z!$w=5gD2eniiDhL68;gIv=z6)n;hPIe=CXhI`8PpB&+GJE6XsQE{s@|V?8{AhB zgS5#41N!!5`5);mfaRqc@uyD9YXkn1BoH)#Sn3k=&tmJ2$8W>Yp6lGxet>_Z7uUP` zUcI<`-ak@!7DRwOK@##R^Pj-%Q=yrQ^%2D7?H}oNmeVYaCD1i|H+9l^x!fcx8mx?F6%ua5U}~Y*;7<&gEUl@is!aTM%y^ z@MvO1^CCj07nQWVEWjie5_|M>T}fBbU$?_M>f4o5W>#g66H&M*c!4DXu+ zrb>5VV#-A%*5Dy~gYT^sf^KCWZbI@M^)`+{BWui5kwo*H&rDs;9?F;Uh|OVD%qZ$8 z8R@xNzyEgQ_&>nZmPE2Y!;HQDlf+DThiQ;@4~VvZn9;^O7C%s(A^Kt#k}_sf8a63r z%;I_nen(3gYLQ63Qraz=Ty{p>f}FNj-=2CpbZRCOHP?e?M5?m&5M#@7?}U-TuxLk#D$WET^3 zT^n9-Zf_zE*KAGKr_H(3dO@>g$#t!WITLnbiVikmMHI=W>_(@I7PX|0Is8$lmg63A zXJnA2Fj+}$CHVi8rh*_fbP}mc?j{r!t`k;&GYw5ag#s#6pMyGWE;gf{I6AAus^k{K zu^f0_S68(HbuzIkbz~A+LWG(^<-m0*rG*MJ0;#D`eA%7}Z1rSOhZqzthmzy)igomL zG0u6yJK;}QpdJ277Y#9wb(rTuF;^KTY^cFE+Vx5Jia;|-cW zw{(xDb5{y4Yg0(zd8P9CfuHa3#e>g2At5M{0PGPZ5rygRpQ$ZaN=>-@Ow@s~osCx# zB$ij*O8y^`n9(7Vk?<6gj~537HZU_clbRPillLzYe|!sERAt`(^PK0LGiT0Rm>Fh3 z?gtPNMIaCrH60L86p;Y0-56mI3>jj;OXhl8QOnj!!7@`SGqW;tTLZH@Wo6dxs_pt# zZx=1MH#4`_ZCium|Mxs+1_m!}TkYljeEv=|^PJ~Am*4&OeST-Cv7yP0jA$7m$XPMZ zRi7U}e=P?g^cF(cRx!U(8QHJ;WoUl{Z?&p^&b$Q+(kl@naE%_DQ@g0@@R?8kiV(jJ zp;W%w?Wzp@X2J-BGVX-?!>XZUwpyoy>(}ABfAze^g-gPs6X5zZtY@k9RJfj5knKfi zXcyeSZJujk{b$?~9-+)NaIMt2=DC|*YAAy1f1L=8{Tq}Lqb1yLvIWrNXV4zgHn!FESTXuq)Vh{&ku7$r6?zJJ1i z#DPhJ1}6_mNlm*dJ!5F*u;C-JMvfYN^_a2F?3{7v|MWkciQ3U2c)f&np)Gg^Tvb8$ zf6dU-#_dI`P!sgLf)C-f+z{y7fzF~MFmg3IM0W8APed8ei_md_L+5ZAdK%Wq!gidc z6M4iJ@lW$5d^>-NKa7U+Uj8s&&U}Dev>h!5TI|?^f0lBKxf19(C>%zc;pKt*hw)Z?1gP7OSE6I+ z9>Q}I&{lj5_&S8XK`Tiaw-oF$gR27i55ii9Vg6?1<&kg<%Sh!0LmyBNd(CE_QDlg4 zjJ?jHrLc1u+Ag+>c3nK|LM65Xzk*MT8&DfMLark>llSl{KAzvnPeAK636OHMe;(G} zOy`JIcoFbUUyJGL+ycHF??RvR<+|Ch_N$a5*z;+w1URZf2jIOxv;a?|@G7zvD5ZC! z&|%#~J{9J}3c4l0H9{VeiRMCUG1`sxq9LRet%sEuzv6J=8<@F;{}^~)kMH2VL5E2$ zNt;BZQfIgvdQ4LE6J7#D9FHn;#EJo>V+T+0&hu>svLiJQw;(XF!u)K{xd0<+6Kp z(>+20{8v!EN2#b*ZnNALKk_z&)U`6^*ML?ayvcN+QckoX;ze}ttRk5W62 zp1ulMj-EbxIxW~5YfXr?##ZsD%S$4Sw%6gF?DTog;SS&{;?UBnS?UBl^ozZ~|$T?!>q&@aB z+P?7Oh?$+8z%!LffBL!8)+}q5%_kD!OPxjXy?t%r%h2?haoBlQM1myhi77-5HCasY zrc6_TJVh=t&C<`3=gO;1%S;jC2Ek}B8-hZygR}4!Ayl>-?8b0&cu*qhkNa~9 zuLw!fKz)MT-_YNfXc}Z56r@;(qfDI1rSWOP2zi)cm~n(Df6JT|lxEFFPVD5I#K}7a zr|8rDOIMX~csMuPCtGH^p z##|FrZe1)bG%pNVi*D1eGORMLHLW$T4Z2s~VrVgLHg68vZrE;o%=}o;9_u^S53Luh z?(5O@f*B9Sf0>~2u^4CQaR#@cXydILY75IUV%1Tv;2ZD_uT;N&%jOBqWqeWBMpDay zHu79JK@LJJNkpf7HeBj6;nH3cM$LFFZ*O+2?bl)DI}+M^ZMY;tGV3IJT;4!SS0^}3 zpbek4sDC+Q`O6twUmNzDFiHdycFv2GBMp(p6n(Ose{4uLj?#~kM;S&L4GK~~`UB;G zhC#up_Eg89kb%(yW0I7l*#3#lamZ@vD{e4*a#t6mnx-#`aH;k ze?6SX;fc_LkRc|;GMNr{m~p&$t5CY>-GerXN#I1WEW%)JV)tuD@NxWKzwbUM9Q!Vm z|NQ)5pdajxoB|z%p|Q>gh#hNqbF*oUOk+nV#Ex*C&4eb{^TI7%C(}>+L&Z57D$<;W zpa@IEvWPn)+9CuTkl6f|6h|#77QFLR(f!50e(kki7vEnr`Qhuk)pzj_e=JUYkk5R4 z@Zb}N51$x3cvt`aphz>e;gRt@{?G(;8Bi#i=nNJ+Y^cN7Zd=z+ZwtDH*c^HNnECrC z0%GFFhAl>m#ihEX(o+3Wd8uKsaj9vkd1=s6%TnuNTU*%KFe?!0ClJC%VmhTyqmuJ( zeEhMN4Ua#*;VibPXU_gvfBgzu$%m)jc;nQcUO)Kx7WH-YwE86&e-_xd9gjdh`z8~C zMk`8l+C>9cvw^HOx9bn+WKlv=zKzD&bT9-UvGeF#)G?nf4&D~b0UesGdTlh6OpGZ= z-ttIC$H@Jwf>R>N(>Cj&=ezfUEmleo*K@_)d)^d|?NTS~>ZaiikTSxHL_5P)gPmsa zdHyMMfD^F9qkIWMokoAxY0$Ps@6;FT%k}ko&~|VJqdoqmcKFAaf7d43Kc_VIt{3+V z+QeZAV>m{s4W5ui1xpo_Un8W%(b9M8iB(*klEfLNbAh) zcoPYQ_{)vA+6;M7O!MiKu+t~cb)L3#o=MwR9=$BOEt=2>42xz`>*H7)3&DPUgTTpy z?d>Cet+2(S}TDxHCuuzcQP2h}c9FESoLwPudeq;g3w=&!jy;tW9AN_=}ZgR}dZ+IfvaA z8b5%`9BLcREpA@1a#dSv%f?Nj?K5@EsZ;8xPrtygee@yjJgwVJ6L3%Dj~-`%Uh@-+Z$>4s_kUg{wS2m^;{= zMP@~oS>JanFDF_;Sy^9qsL753G6oQ1^G3dt_7j zzB2dof3yDgESFKWc((WW(@BNh%Xd||UV8Ak7i~*#Pf6L8nAqj>Z@v;_6^w>C!-yUc zGZt5yt?kAGGUg;yMCC*A!0B`vh(H%$j4V|>UG8|tLB0HP$*8jZ)>~U1@94<+b<<0) zaocJ7*|wFYA8>Vd=Cx5n^-LIbHlP zTjo%1JD5~UYul=oD@EIBb@YcHsUyGm6u)}v6n>e;j|=C4pU=UrbP!R^B8wi_38BL& zNS31~yG{ZR;8$swBK#T8AqhEw7zkuw+1BABW;%nBJ_cE^g^SVASWvIurq^FjSa7k% ze~SE9-DeJUpMl_V{+KWrZ0*8(>NFM$3@8ZYIwK974w;`f>Q)QSp##RJERsbKr#(x47;V z2Y$|CrT4-KAra1dVW7Kk(*x$+a*Gu|e}H!LEunYOv=(MUX?9CERh}=2oMXqhw5Nk2 zVj?(SI#aX^766POoe7Cmr&e4*>RD{TXvL|jxnKTLeL}qjH{;UgFNN91ZoEM~sQyts zt{%MM#v>CZ;B9yguEyKOYk3XMW3yOZ8wgs64>LyTZOy?U9YLgHKzw`R0ewf%e{M>0``^pC`8`L(4Mm^2Um_vkGxZX46)z}vbZ8XVb zW{?LTZrS+o!y8*3ZdcXwuE(#rW^2iBp3d5P>)TyjZ{NB%tDU?0_4nU@{owoWf1!S& zejXM5>*PVty>M;CY&;SZ%;S->e=BxTKB#<;gM4Xzj)(l*VnZ#)yJ&t63(6p24hzlC zUA_4k$N69-d?sp=WS=DQeYG2t_ut1@z3@J4vQS;4zNWsauEvXmLbYA}RQ*hC#}jZk zj=&SPtJkVq)otpvcsrgAuZNjTcSB5@!Ti*YMmZyUJ$0Ro57^s{;HP#&e-U_WzJsQY zEG?(O5!LUm9>^ySoR`Rk)- z9$%l(6D&YL9Hk*Zx!4N`f8nXcZ=nkp-dg+=Hv%%)BlNwqd$%a>s&uIb)NkQ+z=gl? zCon&D5Hc8cSmcb>Suuw>tEdxmbR0oXN|20oTqHkC=fe?QT8K#nr_AzUSpG};2J9L` zVTCho7&k&Ufg7)@;i`1YIGueDAk1$eW(1>|&Pc(6jnbncUJW5oJRozy zI;7JJk}1eg2w|6nsh%2yxu*ufj%S@V#sE!^mNV(pS@1rmqu9~rpd}a(XB5@~7o5;& z+Tgz3Q!yE*s=w>lf3s)zbE5s;;_8a^U8&@E>x-U!3}PoF1pXArPeL1=0|EOBJRpBg zpkD+mOTt!!q#PpyiGQNXbHpkf4~lZ9B;+<9q&GS2r~a> zuBz+n?kw)0cE+SOu!r$rMJY}PYSAypEf$Hh$Vdn?rK3naZ)4cXs?`N3kWAZK9?Z_Q z8Jg23Xl|Q?@rH9a6UV3@tB2Gah;e&yt6HrVt1cn+y9Kx(0Ni98xnM@w)$-l zZ-D&?N_hr7f5}lO!&5a{sOk_^qlOc))0E}|itB}Kg2-^<8*c)Zfg=m7v-N@vzk*aG zpfIPAY(*cIF6N z2seNm&lTtdgA`=wMsd%9~0_jI?LFW=eq-iDndf$GwKF698pz3j|JR-m04BUmK1 z%0xz1e-05M>#PDV(oVgV$P&GggNUvLljV9rFX=>91=b6)!GC5Qee&q(fTEqonobBP z-Dz#GT*XK&Hzx=OaY4EuDG1~{AJwCENUy^ZClWm$g2T9Jcm`K&1Z(9M!r}B*(!ej! zEtFQ{HQX}eJ>0#dl@A4$^%#z+IG)6kcgO_#@Ir~QF`Z@E^Z2}EVqzk|v|d-qlf%V;eX_NBE{5q;-Oyf_Ak zNTe}9q%q(HS|;GIz$2YjFZJ6uEtO8%<6wq`SiSW73kBHus|nm(ZZWr%Yvz`5cX8V} ze~E6S2jMyp#9I>14?r|1B=L%ri8Ap>GLnY~f*=l)0zO}uAUdUKXd0eLX7a^S6{^BD zq=ug(REyMBjO(EMd?H7 zf;5I&Ckr+4S8HWT^}6$Xc~=>E{JXXuf82FD;DRWWXCSJZu+tx5f?O?^;p z!nfla?#4P#eb?>k8TCszo!f5Oc?{pRqkC!TRD3U@oSv+>Z@b3OQ+58X z?i0IuVr?0$9}IZlDQ7158j(dS;fY}7d6FaY$U%5Vi{9R1T5jM4ky!O8GQ=#%VPU*= ztX+;Y@(d{e__Ky)S+#f!(4Tszf2~hhUGUXtozcu4ZwbZ%60iUcsAD!_=R)A{oPZK= z0ylspiUV{5qyc&*dKezY<>P#=T4(|bSP;BMT%)^3yhj%UI>ymBG&r85;K9^cVwDhw z9jx1Tj3#$vFCKIF_zM$nU-!G4=P%T1ytH+8fA?D9*t>tc zV?Q^g`^@TbF>Goi+C>VKvX}YD20zlHd{b7z>lY1y9iGO765QSVF1vuH6Ew>YXF?Lw=yuY zy`4+-;ivB10cC;8U4Q$gf5(32f_?>*;52GSsYo&s@iYg~pmXvx5Rs-U)-5B;cwRf4 zgTcJX{h{kd$YaNLX={ivu!a%6;2g?Q5sfDj{UV%R)L*V14L{ZRb(-e zA0vZ_nlz-Q#cf(94ZRY`s5evFm_}%r)^oChv+IK80o(wcq8lJ9fAUaWrd-3_$}QF{ zl9zESbSvaLxey*3NHC5d@i>_zN(1#n@n|wlnyGh7bM^D3MSvadAT9VlVrLl(0ytO- zj%VO*2wsAh;vui9OAo0_0pWE?%APA-PSClnE~gdf5<89D+k1Zb%{ne}n}6AcJBWiL=OXVW@7H zKFg41Dj@kno_?aiX_`qRHaC+@7p93bb%)d7BrfA7cj!DytjA7Dlb9c>m_;&_T2$66_L zv};u^gT$Xw2qMC>?izIe@C{U-|De(i`m3Ecob@s9I|M! z90h&)Dwe0A4b3#|c}zS@C=^qX-Q1AwBRji~&@X{IuL``nWS6=GuiS-Kez#*6v(3Gb z!;*ksD{?wRB+hC;LW_Bw9xb;?k@5(Dt=YC-*h&q`chF7uA!j&h#00Mo-WE()#lnB0 z)Q-G+e-G_`<>lRn)DPiY`>FaNI7idjBS+4X+q4w)=R z-|`#&6)j&1gnaaX!eYnQA}4~k0os&p@<5?Ke?*Nj+!$e~oF*4?lZ71FDbEC?a+5Ge zE|)1@xm8#yER$Qg79mQ9^c+MmUWCL1;6(@i3XY8Qyeu1$8N7mbNMS~cS>a;^MO1W( z6tC|uCm0m7Vjj(nB$<2$oQkNAj5Vg2DYlu&Ir%&|BILjUHb-(wPJOOC+2}Mo%`-TN ze^JI_a}_s-xcJ#Zxmd0%mn!v@a;0GbnvWNA3&{e$QCK7{(k+ncrG>_&#--+FZZ%m0 z*k_ggHp6=JCVrdwN%OUSti#|AxL59582t4F{Wz?yRn=G3msL2u+W0f{4e`x#{w%Yf zUVDvqT9F=a(Gh{!sVq7qqvids(O{aOe}$v)-Py-P1I$z4yN~Rmja}RjT3@(ug5S<+ zX{GGah=!B-o9txs({u#gZ?U`TWT5GJ4cYYE-I>a8%0?C9vp zgj`8LeW7wEP=nDl$YGff$Bp<4u+yoUolXTiol5O=>JPHh7cOI`S}3>DC~l)4f5G&_ zpaEwZ&oMopP1tofqhtEkuZuGvxxVK*$G6zLvXIm?{^`bzGfLB`*SN zUIf;>n!A^~N7yJI;0_3Ha|g*gLX^PiiO3rSSu*J1%jn?3NC+P;L`V^OyTM_ks4JdJ zB(Z#g5GTgz5~M`1>{vs*F^df2e}_p~)SkHsB%gQkIlf;ZtJjXvPwCi6mcXF?gxA-@OMa(ZEF#Qk58*u!z_Rl^&{@JJP>U+ok{Pl4- z4YZQE^!;5MY3-UzYlZ5!zFJ`vDsc`1n+w85lhKS#CSy)ew2>7L``u+*XNopO1R04w zEP|B~qy3dAD=iCmvO?lTf6$jA-@wvhFpf{`Db>Wg?3tvCMe z(lX=uchnEQ0{rnXcF`guD>`t+Gb*P4U4~?{l=SzPfQ-$TnFHAgZlNke_gGtW)HgloNVD1 zL5{ZU;~qty187ct%~$-VkeQ6=dYxNzXV1BK3wWiB4mgt}`m9=K)t*%QDg!wZI2<{E zHt9tH^P)tb&Kb0Gj#cxUlc53SK0gj(Wj_l$?cd0AvJ}E4a!JBqX&MJnR`PNSgcaOc z;aAcIu2tA1J;d4ce}Z1*3`EuqB#Hb$VX!z@=QLK6a^qUE3Zm>C;(Fa?vPrj#JR#WU|sPHzOz0I=L#I>R2rgggQ(V>x8e}?m%Y!ShnAZmn^Q7A|Q zh9%&MorP6)j?!W*J4k@3eRjs8cCzOXOwz1QfE;$z=l5FlNz(@;th+6FM)G;wUp=aR zb;B#wvtGREi8tST;+hA_=%MiLpdj_k|NL3~i=rI5DsA7EE&KWpU}wbTK>up+i+-p- zn&FJs#f0Hze-zdxKg_qGwIMNWmb*gMCFmj}V}qknTwJ6nA^~&-r1`7rpV8m!XutOe z=?r^0{KbeDBVUYqIl41Ox65|G_PLE*kFOujXtf2i=kb|Ck*|&yHvsz?fPC%+g2$}cwrvf(W7_))5mB ziC=fnO7P!0I_Ohv@;j{t{LGWIAdL5Z?$eu~g5LBf4Ig&4ny}f}Y_(Yp&9cE}v+A3X z-Vx%^VNq%h330$t1~*4VN1(>3QVQ*=urv)tXcG75euWt%U?VB&1Y0PuR%AhErbS^``9lR`+W87qvY z^kQ@$+KlhzHc2gp4UU$OmbiO|#OMurqhvKE8VB~{BJ^Q~Fk_fC(jMsuiHb>2M2R>_ zA8#8JJjg!Kk(xeAKh%~LJT`rzzR*6&e~}+HC4Cy6rJre>YMU8+UCfQ?bB#6D@^r|K z##^i#&<4B-KUKw)8#eS+wq9#=hUL=m8%M&k#ACmQbkc3>)tM zxmjq>PhIfW*G@g!cH@o$busQ5l_TyqR$q@pyVh5qT={49?dImR^#5)zeR%5hf33MS zD@T#|YaW_=t+M#hXm9&oTpzgoJ)ji>25oRc}@=zeV{N<8qN(9hDzhPd|{k4 zm7BxOhseH?q6TRrcb{~MbBKgSfA$CxrW0g{?)?aj?SpiHWQ1X2rY_BxO&BhxIL0`e zl=IcX9Nhw;-grB?UHBD5{q@GpCT|PXIRTE=$_DZf_ta?D(%_U7Ko!>fAGhFsNR`^ z9Kmw4KBid-ZgZI0^c%#;Hf2NnUE(^&LrEc#!HC$yA_pjzNMeuCi%B$H_htmW_7m2l z^btsCmhV~gNtO!Oi-ub4)K^BkVq8jPEYAvM7ypFUm(K%!g#d?Lk&M0TA@#TFpKdrf zx9s(K&mZh~c=x`RtqM&L6NZFMGwg-4KnkybcnFiWsJVBKhHv){#|HV#=JmdKHz79vJb z1|4OT$!q;w60HVu`o$NmzGqYC0v}}vU*bKH7Us*?nLd@DCQQ@af5P7)%#T9(d$>MI{etNk zUC80~vgb!boOTpWf1hE76GU>jZD={qAxu&`nbIzKE(17~lXi7~yGuCsw|UfN2&-wF z_B{(z&^Tv&7;2IAEw<&jMSdd2YLK|#uowZEBSVC+$P_(_wDGZ8f#WE%jjW!!a8}yh zpg8TPpPJfx8VRxKv~a3pHWD9;H{jfd?!W&b^#C5c@vgfzf2szKKXrcDtt}6$XTR(I z6Zb~<2di&iw~DJ$$9NiUuHW(EQ)?fzD~IlR{lD1XuW&cC|FP`j>KlVb{{_WJ?C&^^ z954E-|8F_~nNHK~fXs=I)cc1X2x))u8(jsQS z95Z@DSP%D4ph~(9--O-}Pr+UX&?uPm9)AR`pGL=pfA`Q{-RCHmwP6qOb~KId$z6A$ zl}D(Oe-QQ!MO)#$oZE}e3NlI+uSUy-v9Nw9+5+!Z=tsg5Y8NODZNx_LFpTPj()rTq%6t(<=YIB zA=mJ(e{qm;mhq(VylJAT*7QH-$IUMWp`Z~#w^(LaR#@J)eq_tH{lc~}*cewV->cc6kRE0^js+WWw>79-ZFPqhAc?lY4w^ z4#qv?YZK^y#Mh?viM_tIfQ;xlUmLInNI`2;fA2WrYtz%)NnhJ!yC3`is>+Op251k;h%Irl-PEDh?vBB+{m#h@jRir4{wY5qK9pzO@++KIXe0ODve96>d zbZVJv{=B)Kx;aXYtNIEPbKP@Y)0&iuYFFJHw^wmBxRshZrM_u)ZB2zz>6z!Msq>F1 zb=7%uJf52ZE(4m=+zs9uPo0vUk}+KC4j547seglA8-a=H#>V=Qsi~FFKffu(>uGAJ za94R6=D1Vp+>QBc7^Q)7(Zjeh(Cc<9v)#3x1xYE&51=?j8DG1ozS^tQ%&Ye_G`cI5 zss_(ICA+~r-$#mn4{1#V zTz{4Bd9H?=6i?N~tH^S(yJ239mx;f|t5mxi+_2Z223H-ho(#MLTQChI269eT8a;}u zZjn+C3WEur*^MBv8W4d?sQ{v77};3u_RFcF!ZWWPhSLF!)v!89*BTXT#3jLE zm5R&j^;FcjU_ZIiQ_(cfUDxPpr1VtP)PI7K2GSMT1f|qd)wlq}AD6@^X>iv!cq*GJ z+-#Z38eptuc2lF9(jfPlkPK?BsBNmGlq{%etoAfD0x|Pyd>hdn8Z=SDvQ1tXPuWRU z=D8`?GBXfwb#lNS$#lcwpc)M8l79(Q z4OLAIb+DnEO|0}NUQe>(ZJIsTUC~H;D1TL+S}-=sP=%+ivWD{E9VyFYu$*hQXTF

G58?6J1h<4Vb`OwU=MOH^)y$?`l9^UF{;J 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 delta 34709 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 r8cQ62dKwyWV}Xek^{k!)$h+0) -% * weighting: none(0), linear(1), squared(2), state(3), scale(4) +% * weighting: none(0), linear(1), squared(2), state(3), scale(4), rsqrt(5) % ut {handle|'i'} input function: u_t = ut(t) or character: % * 'i' delta impulse input % * 's' step input / load vector / source term % * 'h' havercosine decaying exponential chirp input % * 'a' sinc (cardinal sine) input % * 'r' pseudo-random binary input -% us {vector|0} steady-state input (1 or M rows) -% xs {vector|0} steady-state and nominal initial state x_0 (1 or N rows) -% um {matrix|1} input scales (1 or M rows) -% xm {matrix|1} initial-state scales (1 or N rows) +% us {vector|0} steady-state input (1 or #inputs rows) +% xs {vector|0} steady-state and nominal initial state x_0 (1 or #states rows) +% um {matrix|1} input scales (1 or #inputs rows) +% xm {matrix|1} initial-state scales (1 or #states rows) % dp {handle|@mtimes} inner product or kernel: xy = dp(x,y) % % RETURNS: @@ -81,8 +85,8 @@ % % CITE AS: % -% C. Himpe (2021). emgr - EMpirical GRamian Framework (Version 5.9) -% [Software]. Available from https://gramian.de . doi:10.5281/zenodo.4454679 +% C. Himpe (2022). emgr - EMpirical GRamian Framework (Version 5.99) +% [Software]. Available from https://gramian.de . doi:10.5281/zenodo.6457616 % % KEYWORDS: % @@ -94,462 +98,463 @@ % % For more information, see: - % Set Integrator Handle (i.e. for custom solvers) - global ODE; - if not(isa(ODE,'function_handle')), ODE = @ssp2; end%if + % Set Integrator Handle (i.e. for custom solvers) + global ODE; + if not(isa(ODE,'function_handle')), ODE = @ssp2; end%if - % Version Info (and export default local integrator) - if isequal(f,'version'), W = 5.9; return; end%if + % Version Info (and export default local integratorvia ODE) + if isequal(f,'version'), W = 5.99; return; else, fState = f; end%if - % Default Arguments - if (nargin < 6) || isempty(pr), pr = 0.0; end%if - if (nargin < 7) || isempty(nf), nf = 0.0; end%if - if (nargin < 8) || isempty(ut), ut = 'i'; end%if - if (nargin < 9) || isempty(us), us = 0.0; end%if - if (nargin < 10) || isempty(xs), xs = 0.0; end%if - if (nargin < 11) || isempty(um), um = 1.0; end%if - if (nargin < 12) || isempty(xm), xm = 1.0; end%if - if (nargin < 13) || isempty(dp), dp = @mtimes; end%if + % Default Arguments + if (nargin < 6) || isempty(pr), pr = 0.0; end%if + if (nargin < 7) || isempty(nf), nf = 0; end%if + if (nargin < 8) || isempty(ut), ut = 'i'; end%if + if (nargin < 9) || isempty(us), us = 0.0; end%if + if (nargin < 10) || isempty(xs), xs = 0.0; end%if + if (nargin < 11) || isempty(um), um = 1.0; end%if + if (nargin < 12) || isempty(xm), xm = 1.0; end%if + if (nargin < 13) || isempty(dp), dp = @mtimes; end%if %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% SETUP - % System Dimensions - M = s(1); % Number of inputs - N = s(2); % Number of states - Q = s(3); % Number of outputs - P = size(pr,1); % Dimension of parameter - K = size(pr,2); % Number of parameter-samples - - % Time Discretization - dt = t(1); % Time-step width - Tf = t(2); % Time horizon - nt = floor(Tf / dt) + 1; % Number of time-steps plus initial value - - % Gramian Type Uniform Case - w = lower(w); - - % Lazy Output Functional - if isnumeric(g) && isequal(g,1), g = @id; Q = N; end%if + % System Dimensions + nInputs = s(1); % Number of system inputs / controls + nStates = s(2); % Number of system states / degrees of freedom (DoF) + nOutputs = s(3); % Number of system outputs / quantities of interest (QoI) - % Pad Configuration Flag Vector - nf(end + 1:13) = 0; + % Parameter Dimensions + nParams = size(pr,1); % Number of parameters / parameter dimension + nParamSamples = size(pr,2); % Number of parameter samples - % Built-in Input Functions - if not(isa(ut,'function_handle')) + % Time Discretization + tStep = t(1); % Time-step width + tFinal = t(2); % Time horizon + nSteps = floor(tFinal / tStep) + 1; % Number of time-steps - switch lower(ut) - case 's' % Step Input - ut = @(t) 1.0; + % Gramian Type + gramianType = lower(w); - case 'h' % Havercosine Chirp Input - a0 = pi / (2.0 * dt) * Tf / log(4.0 * (dt / Tf)); - b0 = (4.0 * (dt / Tf)) ^ (1.0 / Tf); - ut = @(t) 0.5 * cos(a0 * (b0 ^ t - 1.0)) + 0.5; + % Flag Vector + flags = [nf(:)',zeros(1,max(13,13-numel(nf)))]; - case 'a' % Sinc Input - ut = @(t) sin(t / dt) / ((t / dt) + (t == 0)); + % Built-in Input Functions + if not(isa(ut,'function_handle')) - case 'r' % Binary Input - rt = randi([0,1],1,nt); - ut = @(t) rt(:,floor(t / dt) + 1); + a0 = (pi / (2.0 * tStep)) * tFinal / log(4.0 * (tStep / tFinal)); + b0 = (4.0 * (tStep / tFinal)) ^ (1.0 / tFinal); - otherwise % Impulse Input - ut = @(t) (t <= dt) / dt; - end%switch - end%if - - % Lazy Optional Arguments - if isscalar(us), us = repmat(us,M,1); end%if - if isscalar(xs), xs = repmat(xs,N,1); end%if - if isscalar(um), um = repmat(um,M,1); end%if - if isscalar(xm), xm = repmat(xm,N,1); end%if + switch lower(ut) + case 'i', fExcite = @(t) (t <= tStep) / tStep; % Impulse Input + case 's', fExcite = @(t) 1.0; % Step Input + case 'h', fExcite = @(t) 0.5 * cos(a0 * (b0 ^ t - 1.0)) + 0.5; % Havercosine Chirp Input + case 'a', fExcite = @(t) sin(t / tStep) / ((t / tStep) + (t == 0)); % Sinc Input + case 'r', fExcite = @(t) randi([0,1],1,1); % Pseudo-Random Binary Input + otherwise, error(' emgr: Unknown input ut!'); + end%switch + else + fExcite = ut; + end%if %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% CONFIGURATION - % Trajectory Weighting - switch nf(13) - case 1 % Linear time-weighting - wei = @(m) sqrt(0:dt:Tf); - - case 2 % Quadratic time-weighting - wei = @(m) (0:dt:Tf) * sqrt(0.5); - - case 3 % State-weighting - wei = @(m) 1.0 ./ max(sqrt(eps),vecnorm(m,2,1)); - - case 4 % Scale-weighting - wei = @(m) 1.0 ./ max(sqrt(eps),vecnorm(m,Inf,2)); - - otherwise % None - wei = @(m) 1.0; - end%switch - - % Trajectory Centering - switch nf(1) - case 1 % Steady state / output - avg = @(m,s) s; - - case 2 % Final state / output - avg = @(m,s) m(:,end); - - case 3 % Temporal mean of state / output - avg = @(m,s) mean(m,2); - - case 4 % Temporal root-mean-square of state / output - avg = @(m,s) sqrt(mean(m .* m,2)); - - case 5 % Temporal mid-range of state / output - avg = @(m,s) 0.5 * (max(m,[],2) + min(m,[],2)); - - otherwise % None - avg = @(m,s) 0; - end%switch + % Output Function + if (isnumeric(g) && (1 == g)) || ... + (strcmp(gramianType,'c') && not(flags(7))) || strcmp(gramianType,'y') + fOutput = @id; + fAdjoint = g; + else + fOutput = g; + end%if + + % Trajectory Weighting + tInstances = [0.5*tStep,tStep:tStep:tFinal]; + switch flags(13) + case 1, fWeight = @(traj) traj .* sqrt(tInstances); % Linear time-weighting + case 2, fWeight = @(traj) traj .* (tInstances ./ sqrt(2.0)); % Quadratic time-weighting + case 3, fWeight = @(traj) traj ./ max(sqrt(eps),vecnorm(traj,2,1)); % State-weighting + case 4, fWeight = @(traj) traj ./ max(sqrt(eps),vecnorm(traj,Inf,2)); % Scale-weighting + case 5, fWeight = @(traj) traj ./ (pi*tInstances).^0.25; % Reciprocal square-root time-weighting + otherwise, fWeight = @(traj) traj; + end%switch + + % Trajectory Centering + switch flags(1) + case 1, fCenter = @(traj,xs) traj - xs; % Steady state / output + case 2, fCenter = @(traj,xs) traj - traj(:,end); % Final state / output + case 3, fCenter = @(traj,xs) traj - mean(traj,2); % Temporal mean of state / output + case 4, fCenter = @(traj,xs) traj - sqrt(mean(traj .* traj,2)); % Temporal root-mean-square of state / output + case 5, fCenter = @(traj,xs) traj - 0.5*(max(traj,[],2)+min(traj,[],2));% Temporal mid-range of state / output + otherwise, fCenter = @(traj,xs) traj; + end%switch + + % Steady State + vSteadyInput = repmat(us,iif(isscalar(us),nInputs,1),1); + vSteadyState = repmat(xs,iif(isscalar(xs),nStates,1),1); + + % Gramian Normalization + if ismember(flags(6),[1,2]) && ismember(gramianType,{'c','o','x','y'}) + + if 2 == flags(6) % Jacobi-type preconditioner + NF = nf; + NF(6) = 0; + if isequal(w,'c'), NF(7) = 0; end%if + PR = mean(pr,2); + DP = @(x,y) sum(x(1:nStates,:) .* y(:,1:nStates)',2); % Diagonal-only pseudo-kernel + TX = sqrt(abs(emgr(f,g,s,t,w,PR,NF,ut,us,xs,um,xm,DP))); + else % Steady-state preconditioner + TX = vSteadyState; + end%if - % Gramian Normalization - if nf(6) && ismember(w,{'c','o','x','y'}) + TX(abs(TX) nStates + vParamInit = vParamInit + vUnit(nStates+1:end); + end%if + vSteadyOutput = fOutput(vSteadyState,vSteadyInput,vParamInit,0); + mTraj = ODE(fState,fOutput,t,vInit,fSteady,vParamInit); + mTraj = fWeight(fCenter(mTraj,vSteadyOutput)) ./ sPerturb; + if flags(7) + obsCache(:,n) = sum(mTraj,1)'; + else + obsCache(:,n) = reshape(mTraj',[],1); + end%if + end%if + end%for + W = W + dp(obsCache',obsCache); + end%for + end%for + W = W * (tStep / (nStateScales * nParamSamples)); -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Empirical Cross Gramian - case 'x' + case 'x' - assert(isequal(M,Q) || nf(7),' emgr: non-square system!'); + assert((nInputs == nOutputs) || flags(7),' emgr: non-square system!'); - i0 = 1; % Start column index - i1 = A; % Final column index + colFirst = 1; % Start partition column index + colLast = nTotalStates; % Final partition column index - % Partitioned Cross Gramian - if nf(11) > 0 - sp = round(nf(11)); % Partition size - ip = round(nf(12)); % Partition index - i0 = i0 + (ip - 1) * sp; - i1 = min(i0 + sp - 1,N); - if i0 > N - i0 = i0 - (ceil(N / sp) * sp - N); - i1 = min(i0 + sp - 1,A); - end%if + % Partitioned Cross Gramian + if flags(11) > 0 + parSize = round(flags(11)); % Partition size + parIndex = round(flags(12)); % Partition index + colFirst = colFirst + (parIndex - 1) * parSize; + colLast = min(colFirst + (parSize - 1),nStates); + if colFirst > nStates + colFirst = colFirst - (ceil(nStates / parSize) * parSize - nStates); + colLast = min(colFirst + parSize - 1,nTotalStates); + end%if - if (ip < 1) || (i0 > i1) || (i0 < 0) - return; - end%if + if (parIndex < 1) || (colFirst > colLast) || (colFirst < 0) + return; + end%if + end%if + + obsCache = zeros(nSteps*nPages,colLast-colFirst+1); + for k = 1:nParamSamples + vParam = pr(:,k); + for d = 1:nStateScales + for n = 1:(colLast-colFirst+1) % (parallelizable with `parfor`) + sPerturb = mStateScales(colFirst+n-1,d); + if not(0 == sPerturb) + vUnit = sparse(colFirst+n-1,1,sPerturb,nTotalStates,1); + vInit = vSteadyState + vUnit(1:nStates); + vParamInit = vParam; + if nTotalStates > nStates + vParamInit = vParamInit + vUnit(nStates+1:end); + end%if + vSteadyOutput = fOutput(vSteadyState,vSteadyInput,vParamInit,0); + mTraj = ODE(fState,fOutput,t,vInit,fSteady,vParamInit); + mTraj = fWeight(fCenter(mTraj,vSteadyOutput)) ./ sPerturb; + if flags(7) + obsCache(:,n) = sum(mTraj,1)'; + else + obsCache(:,n) = reshape(mTraj',[],1); + end%if end%if - - o = zeros(R*nt, i1-i0+1); % Pre-allocate observability cache - for k = 1:K - for d = 1:D - for n = 1:i1-i0+1 % parfor - if not(xm(n,d) == 0) - en = sparse(i0+n-1,1,xm(i0+n-1,d),N+P,1); - xnd = xs + en(1:N); - pnd = pr(:,k) + en(N+1:end); - y = ODE(f,g,t,xnd,up,pnd); - y = y .* wei(y); - y = y - avg(y,g(xs,us,pnd,0)); - y = y ./ xm(i0+n-1,d); - o(:,n) = oavg(y'); - end%if - end%for - for c = 1:C % parfor - for m = 1:M - if not(um(m,c) == 0) - em = sparse(m,1,um(m,c),M,1); - umc = @(t) us + ut(t) .* em; - x = ODE(f,@id,t,xs,umc,pr(:,k)); - x = x .* wei(x); - x = x - avg(x,xs); - x = x ./ um(m,c); - W = W + dp(x,o(nt*(S*(m-1)) + (1:nt),:)); - end%if - end%for - end%for - end%for + end%for + for c = 1:nInputScales % (parallelizable with `parfor`) + for m = 1:nInputs + sPerturb = mInputScales(m,c); + if not(0 == sPerturb) + vUnit = sparse(m,1,sPerturb,nInputs,1); + fInput = @(t) vSteadyInput + vUnit * fExcite(t); + mTraj = ODE(fState,@id,t,vSteadyState,fInput,vParam); + mTraj = fWeight(fCenter(mTraj,vSteadyInput)) ./ sPerturb; + nBlock = iif(flags(7),0,(m - 1) * nSteps); + W = W + dp(mTraj,obsCache(nBlock+1:nBlock+nSteps,:)); + end%if end%for - W = W * (dt / (C * D * K)); + end%for + end%for + end%for + W = W * (tStep / (nInputScales * nStateScales * nParamSamples)); -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Empirical Linear Cross Gramian - case 'y' - - assert(isequal(M,Q) || nf(7),' emgr: non-square system!'); - assert(isequal(C,size(vm,2)),' emgr: scale count mismatch!'); - - a = zeros(N*nt,Q); % Pre-allocate adjoint cache - for k = 1:K - for c = 1:C - for q = 1:Q % parfor - if not(vm(q,c) == 0) - em = sparse(q,1,vm(q,c),Q,1); - vqc = @(t) us + ut(t) .* em; - z = ODE(g,@id,t,xs,vqc,pr(:,k)); - z = z .* wei(z); - z = z - avg(z,xs); - z = z ./ vm(q,c); - a(:,q) = z(:); - end%if - end%for - if nf(7) - a(:,1) = sum(a,2); - end%if - for m = 1:M % parfor - if not(um(m,c) == 0) - em = sparse(m,1,um(m,c),M,1); - umc = @(t) us + ut(t) .* em; - x = ODE(f,@id,t,xs,umc,pr(:,k)); - x = x .* wei(x); - x = x - avg(x,xs); - x = x ./ um(m,c); - W = W + dp(x,reshape(a(:,1+S*(m-1)),N,nt)'); - end%if - end%for - end%for - end%for - W = W * (dt / (C * K)); + case 'y' + + assert((nInputs == nOutputs) || flags(7),' emgr: non-square system!'); + assert(nInputScales == nOutputScales,' emgr: scale count mismatch!'); + + adjCache = zeros(nSteps,nStates,nPages); + for k = 1:nParamSamples + vParam = pr(:,k); + for c = 1:nInputScales + for q = 1:nOutputs % (parallelizable with `parfor`) + sPerturb = mOutputScales(q,c); + if not(0 == sPerturb) + vUnit = sparse(q,1,sPerturb,nOutputs,1); + fInput = @(t) vSteadyInput + vUnit * fExcite(t); + mTraj = ODE(fAdjoint,@id,t,vSteadyState,fInput,vParam); + mTraj = fWeight(fCenter(mTraj,vSteadyInput)) ./ sPerturb; + adjCache(:,:,q) = mTraj'; + end%if + end%for + if flags(7) + adjCache(:,:,1) = sum(adjCache,3); + end%if + for m = 1:nInputs % (parallelizable with `parfor`) + sPerturb = mInputScales(m,c); + if not(0 == sPerturb) + vUnit = sparse(m,1,sPerturb,nInputs,1); + fInput = @(t) vSteadyInput + vUnit * fExcite(t); + mTraj = ODE(fState,@id,t,vSteadyState,fInput,vParam); + mTraj = fWeight(fCenter(mTraj,vSteadyInput)) ./ sPerturb; + W = W + dp(mTraj,adjCache(:,:,iif(flags(7),1,m))); + end%if + end%for + end%for + end%for + W = W * (tStep / (nInputScales * nParamSamples)); -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Empirical Sensitivity Gramian - case 's' + case 's' - % Controllability Gramian - [pr,pm] = pscales(pr,nf(9),C); - WC = emgr(f,g,s,t,'c',pr,nf,ut,us,xs,um,xm,dp); + % Controllability Gramian + [pr,mParamScales] = paramScales(pr,flags(9),nInputScales); + WC = emgr(f,g,s,t,'c',pr,flags,ut,us,xs,um,xm,dp); - if not(nf(10)) % Input-state sensitivty gramian - DP = @(x,y) sum(sum(x .* y')); % Trace pseudo-kernel - else % Input-output sensitivity gramian - DP = @(x,y) sum(reshape(y,R,[])); % Custom pseudo-kernel - Y = emgr(f,g,s,t,'o',pr,nf,ut,us,xs,um,xm,DP); - DP = @(x,y) abs(sum(y(:) .* Y(:))); % Custom pseudo-kernel - end%if + if not(flags(10)) % Input-state sensitivity gramian + DP = @(x,y) sum(sum(x .* y')); % Trace pseudo-kernel + else % Input-output sensitivity gramian + DP = @(x,y) y; % Custom pseudo-kernel + flags(7) = 1; + Y = emgr(f,g,s,t,'o',pr,flags,ut,us,xs,um,xm,DP); + flags(7) = 0; + DP = @(x,y) abs(sum(y(:) .* Y(:))); % Custom pseudo-kernel + end%if - % (Diagonal) Sensitivity Gramian - WS = zeros(P,1); - for p = 1:P % parfor - pp = repmat(pr,[1,size(pm,2)]); - pp(p,:) = pp(p,:) + pm(p,:); - WS(p) = emgr(f,g,s,t,'c',pp,nf,ut,us,xs,um,xm,DP); - end%for - W = {WC,WS}; + % (Diagonal) Sensitivity Gramian + WS = zeros(nParams,1); + for p = 1:nParams % (parallelizable with `parfor`) + paramSamples = repmat(pr,[1,size(mParamScales,2)]); + paramSamples(p,:) = paramSamples(p,:) + mParamScales(p,:); + WS(p) = emgr(f,g,s,t,'c',paramSamples,flags,ut,us,xs,um,xm,DP); + end%for + + W = {WC,WS}; -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Empirical Identifiability Gramian - case 'i' + case 'i' - % Augmented Observability Gramian - [pr,pm] = pscales(pr,nf(9),D); - V = emgr(f,g,s,t,'o',pr,nf,ut,us,xs,um,[xm;pm],dp); + % Augmented Observability Gramian + [pr,mParamScales] = paramScales(pr,flags(9),nStateScales); + V = emgr(f,g,s,t,'o',pr,flags,ut,us,xs,um,[mStateScales;mParamScales],dp); - WO = V(1:N, 1:N); % Observability gramian - WM = V(1:N, N+1:N+P); % Mixed block + % Return Augmented Observability Gramian + if flags(11), W = V; return; end%if - % Identifiability Gramian - if not(nf(10)) % Schur-complement via approximate inverse - WI = V(N+1:N+P, N+1:N+P) - (WM' * ainv(WO) * WM); - else % Coarse Schur-complement via zero - WI = V(N+1:N+P, N+1:N+P); - end%if - W = {WO,WI}; + WO = V(1:nStates, 1:nStates); % Observability gramian + WM = V(1:nStates, nStates+1:end); % Mixed block + WI = V(nStates+1:end, nStates+1:end); % Parameter gramian -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Empirical Joint Gramian + % Identifiability Gramian via Schur Complement + switch flags(10) + case 0, WI = WI - (WM' * ainv(WO) * WM); + case 2, WI = WI - (WM' * pinv(WO) * WM); + end%switch - case 'j' + W = {WO,WI}; - % Joint Gramian - [pr,pm] = pscales(pr,nf(9),D); - V = emgr(f,g,s,t,'x',pr,nf,ut,us,xs,um,[xm;pm],dp); +%% Empirical Joint Gramian - % Joint Gramian Partition - if nf(11) - W = V; - return; - end%if + case 'j' - WX = V(1:N, 1:N); % Cross gramian - WM = V(1:N, N+1:N+P); % Mixed block + % Joint Gramian + [pr,mParamScales] = paramScales(pr,flags(9),nStateScales); + V = emgr(f,g,s,t,'x',pr,flags,ut,us,xs,um,[mStateScales;mParamScales],dp); - % Cross-Identifiability Gramian - if not(nf(10)) % Schur-complement via approximate inverse - WI = 0.5 * (WM' * ainv(WX + WX') * WM); - else % Coarse Schur-complement via identity - WI = 0.5 * (WM' * WM); - end%if - W = {WX,WI}; + % Return Joint Gramian (Partition) + if flags(11), W = V; return; end%if - otherwise + WX = V(1:nStates, 1:nStates); % Cross gramian + WM = V(1:nStates, nStates+1:end); % Mixed block - error(' emgr: unknown empirical gramian type!'); - end%switch -end + % Cross-Identifiability Gramian via Schur Complement + switch flags(10) + case 1, WI = 0.5 * (WM' * WM); + case 2, WI = 0.5 * (WM' * pinv(WX + WX') * WM); + otherwise, WI = 0.5 * (WM' * ainv(WX + WX') * WM); + end%switch -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% LOCAL FUNCTION: scales + W = {WX,WI}; -function s = scales(nf1,nf2) -% summary: Input and initial state perturbation scales + otherwise - switch nf1 - case 1 % Linear - s = [0.25, 0.50, 0.75, 1.0]; - - case 2 % Geometric - s = [0.125, 0.25, 0.5, 1.0]; - - case 3 % Logarithmic - s = [0.001, 0.01, 0.1, 1.0]; + error(' emgr: unknown empirical gramian type!'); + end%switch +end - case 4 % Sparse - s = [0.01, 0.50, 0.99, 1.0]; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% LOCAL FUNCTION: iif - otherwise % One - s = 1.0; - end%switch +function r = iif(pre,con,alt) +% summary: inline if - if isequal(nf2,0), s = [-s,s]; end%if + if pre, r = con; else, r = alt; end%if end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% LOCAL FUNCTION: pscales +%% LOCAL FUNCTION: id -function [pr,pm] = pscales(p,nf,ns) -% summary: Parameter perturbation scales +function x = id(x,u,p,t) +% summary: (Output) identity functional - assert(size(p,2) >= 2,' emgr: min and max parameter required!'); +end - [pmin,pmax] = bounds(p,2); +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% LOCAL FUNCTION: stateScales - switch nf - case 1 % Linear centering and scaling - pr = 0.5 * (pmax + pmin); - pm = (pmax - pmin) * linspace(0,1.0,ns) + (pmin - pr); +function mScales = scales(flScales,flRot) +% summary: Input and initial state perturbation scales - case 2 % Logarithmic centering and scaling - lmin = log(pmin); - lmax = log(pmax); - pr = real(exp(0.5 * (lmax + lmin))); - pm = real(exp((lmax - lmin) * linspace(0,1.0,ns) + lmin)) - pr; + switch flScales + case 1, mScales = [0.25, 0.50, 0.75, 1.0]; % Linear + case 2, mScales = [0.125, 0.25, 0.5, 1.0]; % Geometric + case 3, mScales = [0.001, 0.01, 0.1, 1.0]; % Logarithmic + case 4, mScales = [0.01, 0.50, 0.99, 1.0]; % Sparse + otherwise, mScales = 1.0; % One + end%switch - otherwise % No centering and linear scaling - pr = pmin; - pm = (pmax - pmin) * linspace(1.0 / ns,1.0,ns); - end%switch + if 0 == flRot, mScales = [-mScales,mScales]; end%if end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% LOCAL FUNCTION: id +%% LOCAL FUNCTION: paramScales -function x = id(x,u,p,t) -% summary: (Output) identity functional +function [vParamSteady,mParamScales] = paramScales(p,flScales,nParamScales) +% summary: Parameter perturbation scales + [vParamMin,vParamMax] = bounds(p,2); + + switch flScales + case 1 % Linear centering and scaling + assert(size(p,2) >= 2,' emgr: min and max parameter required!'); + vParamSteady = 0.5 * (vParamMax + vParamMin); + vScales = linspace(0.0,1.0,nParamScales); + case 2 % Logarithmic centering and scaling + assert(size(p,2) >= 2,' emgr: min and max parameter required!'); + vParamSteady = sqrt(vParamMax .* vParamMin); + vParamMin = log(vParamMin); + vParamMax = log(vParamMax); + vScales = linspace(0.0,1.0,nParamScales); + case 3 % Nominal centering and scaling + assert(size(p,2) == 3,' emgr: min, nom, max parameter required!'); + vParamSteady = p(:,2); + vParamMin = p(:,1); + vParamMax = p(:,3); + vScales = linspace(0.0,1.0,nParamScales); + otherwise % No centering and linear scaling + assert(size(p,2) >= 2,' emgr: min and max parameter required!'); + vParamSteady = vParamMin; + vParamMin = ones(size(p,1),1)./nParamScales; + vScales = linspace(1.0/nParamScales,1.0,nParamScales); + end%switch + + mParamScales = (vParamMax - vParamMin) * vScales + vParamMin; + if 2 == flScales, mParamScales = exp(mParamScales); end%if + mParamScales = mParamScales - vParamSteady; end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -558,13 +563,12 @@ function x = ainv(m) % summary: Quadratic complexity approximate inverse matrix - % Based on truncated Neumann series: X = D^-1 - D^-1 (M - D) D^-1 - D = diag(m); - k = find(abs(D) > sqrt(eps)); - D(k) = 1.0 ./ D(k); - x = m .* (-D); - x = x .* (D'); - x(1:numel(D) + 1:end) = D; + % Based on truncated Neumann series: X = D^-1 - (D^-1 (M - D) D^-1) + D = diag(m); + k = find(abs(D) > sqrt(eps)); + D(k) = 1.0 ./ D(k); + x = (m .* (-D)) .* D'; + x(1:numel(D) + 1:end) = D; end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -573,28 +577,24 @@ function y = ssp2(f,g,t,x0,u,p) % summary: Low-Storage Strong-Stability-Preserving Second-Order Runge-Kutta - global STAGES; % Configurable number of stages for enhanced stability - if not(isscalar(STAGES)), STAGES = 3; end%if - - dt = t(1); - nt = floor(t(2) / dt) + 1; - y0 = g(x0,u(0),p,0); - y = zeros(numel(y0),nt); % Pre-allocate trajectory - y(:,1) = y0; - - xk1 = x0; - xk2 = x0; - for k = 2:nt - tk = (k - 1.5) * dt; - uk = u(tk); - for s = 1:(STAGES - 1) - xk1 = xk1 + (dt / (STAGES - 1)) * f(xk1,uk,p,tk); - end%for - xk2 = xk2 + dt * f(xk1,uk,p,tk); - xk2 = xk2 / STAGES; - xk2 = xk2 + xk1 * ((STAGES - 1) / STAGES); - xk1 = xk2; - y(:,k) = g(xk1,uk,p,tk); + global STAGES; % Configurable number of stages for enhanced stability + if not(isscalar(STAGES)), nStages = 3; else, nStages = STAGES; end%if + + tStep = t(1); + nSteps = floor(t(2) / tStep) + 1; + y = g(x0,u(0),p,0); + y(:,nSteps) = 0.0; % Pre-allocate trajectory + + xk1 = x0; + for k = 2:nSteps + xk2 = xk1; + tCurr = (k - 1.5) * tStep; + uCurr = u(tCurr); + for s = 2:nStages + xk1 = xk1 + (tStep / (nStages - 1)) * f(xk1,uCurr,p,tCurr); end%for + xk1 = (xk1 * (nStages - 1) + xk2 + tStep * f(xk1,uCurr,p,tCurr)) / nStages; + y(:,k) = g(xk1,uCurr,p,tCurr); + end%for end diff --git a/emgrTest.m b/emgrTest.m index 605c9a7..5ba1e66 100644 --- a/emgrTest.m +++ b/emgrTest.m @@ -1,9 +1,11 @@ %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: emgrTest - run all system tests +tid = tic(); + RUNME; disp(' '); @@ -43,3 +45,5 @@ %estProbe([],'observability','nonlinear') %estProbe([],'minimality','nonlinear') +totalTime = toc(tid) + diff --git a/est.m b/est.m index c17f4ef..f34d3b9 100644 --- a/est.m +++ b/est.m @@ -1,11 +1,11 @@ function [r,m] = est(sys,task,config) %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: est - empirical system theory (emgr frontend) - global ODE; ODE = []; % Custom integrator handle + %global ODE; ODE = []; % Custom integrator handle global STAGES; STAGES = 3; % Default integrator configuration global RANK; RANK = Inf; % Maximum rank of decompositions @@ -23,7 +23,7 @@ persistent GX; sysdim = [sys.M, sys.N, sys.Q]; % System dimension - timdis = [sys.dt, sys.Tf]; % Time discretizations + tdisc = [sys.dt, sys.Tf]; % Time discretizations %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% DEFAULT VALUES @@ -66,8 +66,10 @@ gtimes = @(m) m * m'; - dp = match(config,'kernel',[],{'sum', @kernel_sum; ... % Sum Peudo Kernel - 'trace', @kernel_trace; ... % Trace Peudo Kernel + dp = match(config,'kernel',[],{'lie' @(x,y) x*y - y'*x'; % Lie Bracket kernel + 'hyp' @(x,y) x*x' - y*y'; % Hyperbolic SVD kernel + 'sum', @kernel_sum; ... % Sum Peudo Kernel + 'trace', @kernel_trace; ... % Trace Peudo Kernel 'diagonal', @kernel_diagonal; ... % Diagonal Pseudo Kernel 'dmd', @dmd; ... % DMD Pseudo Kernel 'position', @(x,y) x(1:size(x,1)/2,:) * y(:,1:size(y,2)/2); ... % Position Pseudo Kernel @@ -89,11 +91,12 @@ 'random', 'r'}); % Random-binary input % Choose trajectory weighting - nf(13) = match(config,'weighting',0,{'none', 0; ... % No weighting - 'linear', 1; ... % Linear Time-Weighting - 'quadratic', 2; ... % Quadratic Time-Weighting - 'state', 3; ... % State-Based Weighting - 'scale', 4}); % Scale-Based Weighting + nf(13) = match(config,'weighting',0,{'none', 0; ... % No weighting + 'linear', 1; ... % Linear Time-Weighting + 'quadratic', 2; ... % Quadratic Time-Weighting + 'state', 3; ... % State-Based Weighting + 'scale', 4; ... % Range-Based Weighting + 'rsqrt', 5}); % Reciprocal Square-Root Time-Weighting % Choose trajectory centering nf(1) = match(config,'centering',0,{'none', 0; ... % No Centering @@ -101,7 +104,8 @@ 'final', 2; ... % Final State 'mean', 3; ... % Arithmetic Mean 'rms', 4; ... % Root-Mean-Squared - 'midrange', 5}); % Mid-Range + 'midrange', 5; ... % Mid-Range + 'range' 6;}); % Range-based % Choose perturbation scales nf([2,3]) = match(config,'scales',0,{'single', 0; ... % No Subdivision: [1.0] @@ -133,13 +137,15 @@ % Choose parameter centering nf(9) = match(config,'pcentering',0,{'none', 0; ... % No Scaling 'linear', 1; ... % Linear Scaling and Parameter Centering - 'logarithmic', 2}); % Logarithmic Scaling Parameter Centering + 'logarithmic', 2; ... % Logarithmic Scaling Parameter Centering + 'nominal' 3}); % Linear Scaling and Nominal Parameter Centering % Parameter gramian variant nf(10) = match(config,'ptype',0,{'standard', 0; ... % Regular parameter Gramian 'special', 1; ... % Generic non-standard 'io_sensitivity', 1; ... % input-output-based sensitivity gramian - 'coarse_schur', 1}); % (cross-)identifiability gramian via approximate schur complement + 'coarse_schur', 1; % (cross-)identifiability gramian via coarse schur complement + 'exact_schur' 2}); % (cross-)identifiability gramian via exact schur complement % Set maximum rank for decompositions RANK = hasfield(config,'max_order',Inf); @@ -172,7 +178,7 @@ assert(not(isempty(v)),'est: Unknown matrix_equation method'); - r = emgr(f{v},g{v},sysdim,timdis,w{v},pr,nf,ut,us,xs,um,xm,dp); + r = emgr(f{v},g{v},sysdim,tdisc,w{v},pr,nf,ut,us,xs,um,xm,dp); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% SINGULAR VALUES @@ -185,7 +191,7 @@ assert(not(isempty(v)),'est: Unknown singular_value method'); - r = SVD(emgr(f{v},g{v},sysdim,timdis,w{v},pr,nf,ut,us,xs,um,xm,dp)); + r = SVD(emgr(f{v},g{v},sysdim,tdisc,w{v},pr,nf,ut,us,xs,um,xm,dp)); if hasfield(config,'score',false) @@ -207,20 +213,20 @@ if (isequal(task.method,'dmd_galerkin') || isequal(task.method,'poor_man')) && isequal(task.variant,'observability') - W = {emgr(f{2},g{2},sysdim,timdis,w{2},pr,nf,ut,us,xs,um,xm,dp)}; + W = {emgr(f{2},g{2},sysdim,tdisc,w{2},pr,nf,ut,us,xs,um,xm,dp)}; - elseif (isequal(task.method,'dmd_galerkin') || isequal(task.method,'poor_man')) + elseif (isequal(task.method,'dmd_galerkin') || isequal(task.method,'poor_man')) - W = {emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp)}; + W = {emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp)}; elseif isequal(task.variant,'observability') - W = {emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp), ... - emgr(f{2},g{2},sysdim,timdis,w{2},pr,nf,ut,us,xs,um,xm,dp)}; + W = {emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp), ... + emgr(f{2},g{2},sysdim,tdisc,w{2},pr,nf,ut,us,xs,um,xm,dp)}; elseif isequal(task.variant,'minimality') - W = {emgr(f{3},g{3},sysdim,timdis,w{3},pr,nf,ut,us,xs,um,xm,dp)}; + W = {emgr(f{3},g{3},sysdim,tdisc,w{3},pr,nf,ut,us,xs,um,xm,dp)}; else error('est: Unknown model_reduction variant'); @@ -231,7 +237,7 @@ 'dominant_subspaces', @dominant_subspaces; ... 'approx_balancing', @approx_balancing; ... 'balanced_pod', @balanced_pod; ... - 'balanced_truncation', @balanced_truncation}); + 'balanced_truncation', @balanced_truncation}); assert(not(isempty(reductor)),'est: Unknown model_reduction method'); @@ -269,7 +275,7 @@ assert(not(isempty(w)),'est: Unknown parameter_reduction method'); - W = emgr(sys.f,sys.g,sysdim,timdis,w,pr,nf,ut,us,xs,um,xm,dp); + W = emgr(sys.f,sys.g,sysdim,tdisc,w,pr,nf,ut,us,xs,um,xm,dp); [UP,~,~] = SVD(W{2}); @@ -296,12 +302,12 @@ if isequal(task.method,'observability') - W = [emgr(sys.f,sys.g,sysdim,timdis,'c',pr,nf,ut,us,xs,um,xm,dp), ... - emgr(sys.f,sys.g,sysdim,timdis,'i',pr,nf,ut,us,xs,um,xm,dp)]; + W = [emgr(sys.f,sys.g,sysdim,tdisc,'c',pr,nf,ut,us,xs,um,xm,dp), ... + emgr(sys.f,sys.g,sysdim,tdisc,'i',pr,nf,ut,us,xs,um,xm,dp)]; elseif isequal(task.method,'minimality') - W = emgr(sys.f,sys.g,sysdim,timdis,'j',pr,nf,ut,us,xs,um,xm,dp); + W = emgr(sys.f,sys.g,sysdim,tdisc,'j',pr,nf,ut,us,xs,um,xm,dp); else error('est: Unknown combined_reduction method'); @@ -355,12 +361,12 @@ eg = @(ui,yj,dp) emgr(@(x,u,p,t) sys.f(x,us + sparse(ui,1,u,sys.M,1),p,t), ... @(x,u,p,t) sys.F(x,ys + sparse(yj,1,u,sys.Q,1),p,t), ... - [1,sys.N,1],timdis,'y',pr,nf,ut,[],xs,um,xm,dp); + [1,sys.N,1],tdisc,'y',pr,nf,ut,[],xs,um,xm,dp); else eg = @(ui,yj,dp) emgr(@(x,u,p,t) sys.f(x,us + sparse(ui,1,u,sys.M,1),p,t), ... @(x,u,p,t) elem(sys.g(x,u,p,t),yj), ... - [1,sys.N,1],timdis,'x',pr,nf,ut,[],xs,um,xm,dp); + [1,sys.N,1],tdisc,'x',pr,nf,ut,[],xs,um,xm,dp); end%if em = match(task,'method',[],{'relative_gain_array', @(ui,yj) eg(ui,yj,@kernel_trace); ... @@ -369,7 +375,7 @@ 'participation_matrix', @(ui,yj) sqrt(gtrace(eg(ui,yj,[]))); ... 'hardy_2', @(ui,yj) abs(emgr(@(x,u,p,t) sys.f(x,us + sparse(ui,1,u,sys.M,1),p,t), ... @(x,u,p,t) elem(sys.g(x,u,p,t),yj), ... - [1,sys.N,1],timdis,'c',pr,nf,ut,[],xs,um,xm)); ... + [1,sys.N,1],tdisc,'c',pr,nf,ut,[],xs,um,xm)); ... 'hardy_inf', @(ui,yj) sum(abs(EIG(eg(ui,yj,[])))); ... 'hankel_interaction', @(ui,yj) abs(eigs(eg(ui,yj,[]),1)); ... 'rms_hsv', @(ui,yj) sum(SVD(eg(ui,yj,[])).^4)}); @@ -391,7 +397,7 @@ assert(not(isempty(v)),'est: Unknown state_sensitivity method'); - r = sqrt(abs(emgr(f{v},g{v},sysdim,timdis,w{v},pr,nf,ut,us,xs,um,xm,@kernel_diagonal))); + r = sqrt(abs(emgr(f{v},g{v},sysdim,tdisc,w{v},pr,nf,ut,us,xs,um,xm,@kernel_diagonal))); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% PARAMETER SENSITIVITY @@ -408,7 +414,7 @@ nf(7) = match(task,'method',0,{'observability', 1}); - ws = emgr(sys.f,sys.g,sysdim,timdis,'s',pr,nf,ut,us,xs,um,xm,dp); + ws = emgr(sys.f,sys.g,sysdim,tdisc,'s',pr,nf,ut,us,xs,um,xm,dp); r = ws{2}; @@ -422,7 +428,7 @@ assert(not(isempty(w)),'est: Unknown parameter_identifiability method'); - wi = emgr(sys.f,sys.g,sysdim,timdis,w,pr,nf,ut,us,xs,um,xm,dp); + wi = emgr(sys.f,sys.g,sysdim,tdisc,w,pr,nf,ut,us,xs,um,xm,dp); r = SVD(wi{2}); @@ -438,7 +444,7 @@ nf(7) = v; - [UC,SC,~] = SVD(emgr(sys.f,sys.g,sysdim,timdis,'c',pr,nf,ut,us,xs,um,xm,dp)); + [UC,SC,~] = SVD(emgr(sys.f,sys.g,sysdim,tdisc,'c',pr,nf,ut,us,xs,um,xm,dp)); r = SC; @@ -463,7 +469,7 @@ r = (rx .* rx) ./ (rc .* ro); else - r = arrayfun(@(k) emgr(sys.f,sys.g,sysdim,timdis,w,pr,nf,ut,us,xs,k,k,@kernel_trace),linspace(1.0,10.0,10)); + r = arrayfun(@(k) emgr(sys.f,sys.g,sysdim,tdisc,w,pr,nf,ut,us,xs,k,k,@kernel_trace),linspace(1.0,10.0,10)); end%if %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -471,6 +477,8 @@ case 'gramian_index' % OK + xlogx = @(x) x .* log(x); + inw = match(task,'method',[],{'sigma_min', @(w) min(SVD(w)); ... ... 'harmonic_mean', @(w) size(w,1)/sum(1./SVD(w)) @@ -483,7 +491,9 @@ ... 'sigma_max', @(w) svds(w,1); ... ... - 'log_det', @(w) log(prod(SVD(w))); ... + 'log_det', @(w) sum(log(SVD(w))); ... +... + 'entropy', @(w) -1.0./size(w,1) * sum(xlogx(SVD(w))); ... ... 'storage_efficiency', @(w) sqrt(prod(SVD(w))/prod(diag(w))); ... ... @@ -499,7 +509,7 @@ if isempty(WC) || not(isequal(FC,sys.f)) || not(isequal(GC,sys.g)) - WC = emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp); + WC = emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp); FC = sys.f; GC = sys.g; end%if @@ -510,7 +520,7 @@ if isempty(WO) || not(isequal(FO,sys.f)) || not(isequal(GO,sys.g)) - WO = emgr(f{2},g{2},sysdim,timdis,w{2},pr,nf,ut,us,xs,um,xm,dp); + WO = emgr(f{2},g{2},sysdim,tdisc,w{2},pr,nf,ut,us,xs,um,xm,dp); FO = sys.f; GO = sys.g; end%if @@ -521,7 +531,7 @@ if isempty(WX) || not(isequal(FX,sys.f)) || not(isequal(GX,sys.g)) - WX = emgr(f{3},g{3},sysdim,timdis,w{3},pr,nf,ut,us,xs,um,xm,dp); + WX = emgr(f{3},g{3},sysdim,tdisc,w{3},pr,nf,ut,us,xs,um,xm,dp); FX = sys.f; GX = sys.g; end%if @@ -550,7 +560,7 @@ if isempty(WX) || not(isequal(FX,sys.f)) || not(isequal(GX,sys.g)) - WX = emgr(f{3},g{3},sysdim,timdis,w{3},pr,nf,ut,us,xs,um,xm,dp); + WX = emgr(f{3},g{3},sysdim,tdisc,w{3},pr,nf,ut,us,xs,um,xm,dp); FX = sys.f; GX = sys.g; end%if @@ -572,8 +582,8 @@ if isempty(WC) || not(isequal(FC,sys.f)) || not(isequal(GC,sys.g)) || not(isequal(FO,sys.f)) || not(isequal(GO,sys.g)) nf(7) = 0; - WC = emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp); - WO = emgr(f{2},g{2},sysdim,timdis,w{2},pr,nf,ut,us,xs,um,xm,dp); + WC = emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp); + WO = emgr(f{2},g{2},sysdim,tdisc,w{2},pr,nf,ut,us,xs,um,xm,dp); FC = sys.f; GC = sys.g; FO = sys.f; @@ -598,12 +608,12 @@ if isempty(WQ) || not(isequal(FQ,sys.f)) || not(isequal(GQ,sys.g)) nf(7) = 1; - WQ = emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp); + WQ = emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp); FQ = sys.f; GQ = sys.g; end%if - r = eps + abs(inoc(WQ) - arrayfun(@(k) inoc(emgr(f{1},@(x,u,p,t) g{1}(sys.proj{1}(:,1:k)*(sys.proj{2}(:,1:k)'*x),u,p,t),sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp)),1:min(sys.N,RANK))); + r = eps + abs(inoc(WQ) - arrayfun(@(k) inoc(emgr(f{1},@(x,u,p,t) g{1}(sys.proj{1}(:,1:k)*(sys.proj{2}(:,1:k)'*x),u,p,t),sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp)),1:min(sys.N,RANK))); return end%if @@ -618,8 +628,8 @@ if isempty(WC) || not(isequal(FC,sys.f)) || not(isequal(GC,sys.g)) || not(isequal(FO,sys.f)) || not(isequal(GO,sys.g)) nf(7) = 0; - WC = emgr(f{1},g{1},sysdim,timdis,w{1},pr,nf,ut,us,xs,um,xm,dp); - WO = emgr(f{2},g{2},sysdim,timdis,w{2},pr,nf,ut,us,xs,um,xm,dp); + WC = emgr(f{1},g{1},sysdim,tdisc,w{1},pr,nf,ut,us,xs,um,xm,dp); + WO = emgr(f{2},g{2},sysdim,tdisc,w{2},pr,nf,ut,us,xs,um,xm,dp); FC = sys.f; GC = sys.g; FO = sys.f; @@ -635,9 +645,9 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% TAU FUNCTION - case 'tau_function' + case 'tau_function' % OK - r = arrayfun(@(k) prod(real(EIG(eye(sys.N) + emgr(f{3},g{3},sysdim,timdis,w{3},pr,nf,ut,us,xs,um,xm,@(x,y) x(:,k:end)*y(k:end,:))))),1:floor(sys.Tf / sys.dt)); + r = arrayfun(@(k) prod(real(EIG(eye(sys.N) + emgr(f{3},g{3},sysdim,tdisc,w{3},pr,nf,ut,us,xs,um,xm,@(x,y) x(:,k:end)*y(k:end,:))))),1:floor(sys.Tf / sys.dt)); end%switch ODE = []; @@ -660,7 +670,7 @@ if isinf(RANK) varargout = {svd(A)}; else - varargout = {svds(A,r)}; + varargout = {svds(A,RANK)}; end%if case 2 @@ -668,7 +678,7 @@ if isinf(RANK) [U,D,~] = svd(A); else - [U,D,~] = svds(A,r); + [U,D,~] = svds(A,RANK); end%if D = diag(D); varargout = {U,D}; @@ -678,7 +688,7 @@ if isinf(RANK) [U,D,V] = svd(A); else - [U,D,V] = svds(A,r); + [U,D,V] = svds(A,RANK); end%if D = diag(D); varargout = {U,D,V}; @@ -698,7 +708,7 @@ if isinf(RANK) varargout = {eig(A)}; else - varargout = {eigs(A,r)}; + varargout = {eigs(A,RANK)}; end%if case 2 @@ -706,7 +716,7 @@ if isinf(RANK) [U,D,~] = eig(A,'vector'); else - [U,D,~] = eigs(A,r); + [U,D,~] = eigs(A,RANK); D = diag(D); end%if varargout = {U,D}; @@ -776,9 +786,9 @@ function y = rk45ex(f,g,t,x0,u,p) % summary: Adaptive 4th/5th Dormand-Prince Runge-Kutta method - [S,x] = ode45(@(t,x) f(x,u(t),p,t),[0,t(2)],x0,'InitialStep',t(1)); - z = arrayfun(@(k) g(x(k,:)',u(S(k)),p,S(k)),1:numel(S),'UniformOutput',false); - y = interp1(S,z',0:t(1):t(2))'; + [S,x] = ode45(@(t,x) f(x,u(t),p,t),[0,t(2)],x0,odeset('InitialStep',t(1))); + z = cell2mat(arrayfun(@(k) g(x(k,:)',u(S(k)),p,S(k)),1:numel(S),'UniformOutput',false)); + y = interp1(S,z',(0:t(1):t(2)))'; end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -920,12 +930,14 @@ end%if if isequal(numel(XL),1) + skip_x = 1; max_x = 1; else max_x = min(size(XL,1)-1,size(XL,2)); end%if if isequal(numel(PL),1) + skip_p = 1; max_p = 1; else max_p = min(size(PL,1)-1,size(PL,2)); @@ -937,7 +949,7 @@ norms = { @(y) sys.dt * norm(y(:),1), ... % L1 time series norm @(y) sqrt(sys.dt) * norm(y(:),2), ... % L2 time series norm @(y) norm(y(:),Inf), ... % Linf time series norm - @(y) sum(abs(prod(y,1).^(1/size(y,1))))}; % L0 time series norm + @(y) sum(abs(prod(y,1).^(1/size(y,1)))) }; % L0 time series norm ln = cellfun(@(n) zeros(numel(test_x),numel(test_p)),norms,'UniformOutput',false); diff --git a/est.scm b/est.scm index 63bfa03..cdfa774 100644 --- a/est.scm +++ b/est.scm @@ -1,233 +1,239 @@ -(est "Empirial System Theory (EST)" ;;;; Reference Tree +('est "Empirial System Theory (EST)" ;;;; Reference Tree - (System ;;; System Argument + ('System ;;; System Argument - (function-handle "Vector field") - (function-handle "Output functional") - (function-handle "Adjoint vector field") - (positive-integer "Number of inputs") - (positive-integer "Number of States") - (positive-integer "Number of Outputs") - (positive-real "Time step width") - (positive-real "Time horizon")) + ('function-handle "Vector field") + ('function-handle "Output functional") + ('function-handle "Adjoint vector field") + ('positive-integer "Number of inputs") + ('positive-integer "Number of States") + ('positive-integer "Number of Outputs") + ('positive-real "Time step width") + ('positive-real "Time horizon")) - (Task ;;; Task Argument + ('Task ;;; Task Argument - (matrix_equation "Matrix equation solver" ;; Matrix equation task + ('matrix_equation "Matrix equation solver" ;; Matrix equation task - (lyapunov "Lyapunov Equation") - (sylvester "Sylvester Equation")) + ('lyapunov "Lyapunov Equation") + ('sylvester "Sylvester Equation")) - (singular_values "Singular values of system gramian" ;; Gramian singular value task + ('singular_values "Singular values of system gramian" ;; Gramian singular value task - (controllability "Controllability Gramian") - (observability "Observability Gramian") - (minimality "Cross Gramian")) + ('controllability "Controllability Gramian") + ('observability "Observability Gramian") + ('minimality "Cross Gramian")) - (model_reduction "Model reduction of state space" ;; Model reduction task + ('model_reduction "Model reduction of state space" ;; Model reduction task - (poor_man "Poor man aka POD") - (dominant_subspaces "Dominant Subspaces aka DSPMR") - (approx_balancing "Approximate balancing aka Modified POD") - (balanced_pod "Balanced POD") - (balanced_truncation "Balanced truncation") - (dmd_galerkin "Dynamic mode decomposition Galerkin") + ('poor_man "Poor man aka POD") + ('dominant_subspaces "Dominant Subspaces aka DSPMR") + ('approx_balancing "Approximate balancing aka Modified POD") + ('balanced_pod "Balanced POD") + ('balanced_truncation "Balanced truncation") + ('dmd_galerkin "Dynamic mode decomposition Galerkin") - ((controllability "Controllability gramian only") - (observability "Controllability and observability gramian") - (minimality "Cross Gramian"))) + (('controllability "Controllability gramian only") + ('observability "Controllability and observability gramian") + ('minimality "Cross Gramian"))) - (parameter_reduction "Parameter reduction" ;; Parameter reduction task + ('parameter_reduction "Parameter reduction" ;; Parameter reduction task - (observability "Identifiability gramian") - (minimality "Cross-identifiability gramian")) + ('observability "Identifiability gramian") + ('minimality "Cross-identifiability gramian")) - (combined_reduction "Combined state and parameter reduction" ;; Combined reduction task + ('combined_reduction "Combined state and parameter reduction" ;; Combined reduction task - (poor_man "Poor man") - (dominant_subspaces "Dominant subspaces") - (approx_balancing "Approximate balancing") - (balanced_pod "Balanced POD") - (balanced_truncation "Balanced truncation") + ('poor_man "Poor man") + ('dominant_subspaces "Dominant subspaces") + ('approx_balancing "Approximate balancing") + ('balanced_pod "Balanced POD") + ('balanced_truncation "Balanced truncation") - ((observability "Controllability, observability and identifiability gramian") - (minimality "Cross and cross-identifiability gramian"))) + (('observability "Controllability, observability and identifiability gramian") + ('minimality "Cross and cross-identifiability gramian"))) - (decentralized_control "Decentralized Control" ;; Decentralized control task + ('decentralized_control "Decentralized Control" ;; Decentralized control task - (relative_gain_array "Relative gain array") - (io_coherence "Input-output coherence") - (io_pairing "Input-output pairing") - (hardy_2 "Hardy-2 norm based") - (hardy_inf "Hardy-Infinity norm based") - (participation_matrix "Participation matrix") - (hankel_interaction "Hankel interaction array") - (rms_hsv "Hilbert-Schmidt-Hankel norm based")) + ('relative_gain_array "Relative gain array") + ('io_coherence "Input-output coherence") + ('io_pairing "Input-output pairing") + ('hardy_2 "Hardy-2 norm based") + ('hardy_inf "Hardy-Infinity norm based") + ('participation_matrix "Participation matrix") + ('hankel_interaction "Hankel interaction array") + ('rms_hsv "Hilbert-Schmidt-Hankel norm based")) - (state_sensitivity "State sensitivity" ;; State sensitivity task + ('state_sensitivity "State sensitivity" ;; State sensitivity task - (controllability "Controllability-based") - (observability "Observability-based") - (minimality "Minimality-based")) + ('controllability "Controllability-based") + ('observability "Observability-based") + ('minimality "Minimality-based")) - (parameter_sensitivity "Parameter sensitivity" ;; Parameter sensitivity task + ('parameter_sensitivity "Parameter sensitivity" ;; Parameter sensitivity task - (controllability "Controllability-based") - (observability "Observability-based") - (minimality "Minimality-based")) + ('controllability "Controllability-based") + ('observability "Observability-based") + ('minimality "Minimality-based")) - (parameter_identifiability "Parameter identifiability" ;; Parameter identification task + ('parameter_identifiability "Parameter identifiability" ;; Parameter identification task - (observability "Observability-based") - (minimality "Minimality-based")) + ('observability "Observability-based") + ('minimality "Minimality-based")) - (uncertainty_quantification "Uncertainty Quantification" ;; Uncertainty quantification task + ('uncertainty_quantification "Uncertainty Quantification" ;; Uncertainty quantification task - (controllability "Controllability-based") - (observability "Observability-based")) + ('controllability "Controllability-based") + ('observability "Observability-based")) - (nonlinearity_quantification "Nonlinearity Quantification" ;; Nonlinearity quantification task + ('nonlinearity_quantification "Nonlinearity Quantification" ;; Nonlinearity quantification task - (controllability "Controllability-based") - (observability "Observability-based") - (minimality "Minimality-based") - (correlation "Correlation-based")) + ('controllability "Controllability-based") + ('observability "Observability-based") + ('minimality "Minimality-based") + ('correlation "Correlation-based")) - (gramian_index "Gramian index" ;; Gramian index task + ('gramian_index "Gramian index" ;; Gramian index task - (sigma_min "Minimal singular value") ; -∞ Generalized mean - (harmonic_mean "Harmonic mean of singular values") ; -1 Generalized mean - (geometric_mean "Geometric mean of singular values") ; 0 Generalized mean - (energy_fraction "Relative nuclear norm aka trace norm") ; 1 Generalized mean - (operator_norm "Relative operator norm aka Hilbert-Schmidt norm"); 2 Generalized mean - (sigma_max "Maximum singular value") ; ∞ Generalized mean - (log_det "Logarithm of determinant") - (storage_efficiency "Energy storage efficiency") - (unobservability_index "Unobservability index") - (performance_index "Performance index")) + ('sigma_min "Minimal singular value") ; -∞ Generalized mean + ('harmonic_mean "Harmonic mean of singular values") ; -1 Generalized mean + ('geometric_mean "Geometric mean of singular values") ; 0 Generalized mean + ('energy_fraction "Relative nuclear norm aka trace norm") ; 1 Generalized mean + ('operator_norm "Relative operator norm aka Hilbert-Schmidt norm"); 2 Generalized mean + ('sigma_max "Maximum singular value") ; ∞ Generalized mean + ('log_det "Logarithm of determinant") + ('entropy "SVD-based entropy") + ('storage_efficiency "Energy storage efficiency") + ('unobservability_index "Unobservability index") + ('performance_index "Performance index")) - (system_index "System Index" ;; System index task + ('system_index "System Index" ;; System index task - (cauchy_index "Cauchy Index") ; Discrete - (system_entropy "System Entropy") ; Linear - (gramian_distance "Gramian Distance") ; Linear - (system_symmetry "System Symmetry") ; Logarithmic - (io_coherence "Input-Output Coherence") ; Logarithmic - (system_gain "System Gain") ; Logarithmic - (network_sensitivity "Network Sensitivity") ; Logarithmic - (geometric_mean_hsv "Geometric Mean of Hankel Singular Values") ; Logarithmic - (rv_coefficient "RV Coefficient")) ; Logarithmic + ('cauchy_index "Cauchy Index") ; Discrete + ('system_entropy "System Entropy") ; Linear + ('gramian_distance "Gramian Distance") ; Linear + ('system_symmetry "System Symmetry") ; Logarithmic + ('io_coherence "Input-Output Coherence") ; Logarithmic + ('system_gain "System Gain") ; Logarithmic + ('network_sensitivity "Network Sensitivity") ; Logarithmic + ('geometric_mean_hsv "Geometric Mean of Hankel Singular Values") ; Logarithmic + ('rv_coefficient "RV Coefficient")) ; Logarithmic - (system_norm "Approximate System Norm" ;; System norm task + ('system_norm "Approximate System Norm" ;; System norm task - (hardy_inf_norm "Hardy-Infinity Norm") ; Schatten-1 norm of HSVs - (hilbert_schmidt_hankel_norm "Hilbert-Schmidt-Hankel Norm" ) ; Schatten-2 norm of HSVs - (hankel_norm "Hankel Norm") ; Schatten-∞ norm of HSVs - (hardy_2_norm "Hardy-2 Norm")) ; Via Output controllability Gramian + ('hardy_inf_norm "Hardy-Infinity Norm") ; Schatten-1 norm of HSVs + ('hilbert_schmidt_hankel_norm "Hilbert-Schmidt-Hankel Norm" ) ; Schatten-2 norm of HSVs + ('hankel_norm "Hankel Norm") ; Schatten-∞ norm of HSVs + ('hardy_2_norm "Hardy-2 Norm")) ; Via Output controllability Gramian - (tau_function "Tau Function")) ;; Tau function task + ('tau_function "Tau Function")) ;; Tau function task - (Configuration ;;; Configuration Argument + ('Configuration ;;; Configuration Argument + + ('solver "Solver" ;; Integrator for simulation of training and test trajectories - (solver "Solver" ;; Integrator for simulation of training and test trajectories + ('rk1ex "1st-Order Explicit Runge-Kutta") ; Euler method + ('rk2ex "2nd-Order Explicit Runge-Kutta") ; Heun method (DEFAULT) + ('rk45ex "4th/5th-Order Explicit Runge-Kutta") ; Dormand-Prince method + ('@solver "Custom solver")) ; Custom solver function handle with signature @(f,g,t,x0,u,p) - (rk1ex "1st-Order Explicit Runge-Kutta") ; Euler method - (rk2ex "2nd-Order Explicit Runge-Kutta") ; Heun method (DEFAULT) - (rk45ex "4th/5th-Order Explicit Runge-Kutta") ; Dormand-Prince method - (@(f,g,t,x0,u,p) "Custom solver")) ; Custom solver function handle + ('kernel "Inner Product Kernel" ;; Kernel used for the Gramian computation - (kernel "Inner Product Kernel" ;; Kernel used for the Gramian computation + ('lie "Lie Bracket Pseudo Kernel") ; Pseudo kernel for computing Lie bracket of empirical Gramian + ('hyp "Hyperbolic-SVD Pseudo Kernel") ; Pseudo kernel for computing hyperbolic SVD difference + ('sum "Sum Pseudo Kernel") ; Pseudo kernel for computing the sum of all empirical Gramian elements + ('trace "Trace Pseudo Kernel") ; Pseudo kernel for computing the trace of an empirical Gramian + ('diagonal "Diagonal Pseudo Kernel") ; Pseudo kernel for computing only the diagonal of an empirical Gramian + ('position "Position Pseudo Kernel") ; Pseudo kernel for computing only the upper left diagonal block of half the state-space dimension + ('velocity "Velocity Pseudo Kernel") ; Pseudo kernel for computing only the lower right diagonal block of half the state-space dimension + ('dmd "DMD Pseudo Kernel") ; Pseudo kernel for computing an approximate Koopman operator via dynamic mode decomposition + ('linear "Linear Kernel") ; Linear standard L2 inner product (unit) kernel (DEFAULT) + ('quadratic "Quadratic Polynomial Kernel") ; Second order polynomial kernel with unit shift + ('cubic "Cubic Polynomial Kernel") ; Third order polynomial kernel with unit shift + ('sigmoid "Sigmoid Kernel") ; Sigmoid kernel with unit shift + ('mercersigmoid "Mercer Sigmoid Kernel") ; Mercer variant of Sigmoid kernel with unit shifts + ('logarithmic "Logarithmic Kernel") ; Logarithmic kernel with unit shift + ('exponential "Exponential Kernel") ; Exponential kernel + ('gauss "Gauss Kernel") ; Gaussian kernel with unit covariance + ('single "Single Precision Kernel") ; Linear kernel using single precision floating point numbers + ('@kernel "Custom kernel")) ; Custom kernel function handle with signature @(x,y) - (sum "Sum Pseudo Kernel") ; Pseudo kernel for computing the sum of all empirical Gramian elements - (trace "Trace Pseudo Kernel") ; Pseudo kernel for computing the trace of an empirical Gramian - (diagonal "Diagonal Pseudo Kernel") ; Pseudo kernel for computing only the diagonal of an empirical Gramian - (position "Position Pseudo Kernel") ; Pseudo kernel for computing only the upper left diagonal block of half the state-space dimension - (velocity "Velocity Pseudo Kernel") ; Pseudo kernel for computing only the lower right diagonal block of half the state-space dimension - (dmd "DMD Pseudo Kernel") ; Pseudo kernel for computing an approximate Koopman operator via dynamic mode decomposition - (linear "Linear Kernel") ; Linear standard L2 inner product (unit) kernel (DEFAULT) - (quadratic "Quadratic Polynomial Kernel") ; Second order polynomial kernel with unit shift - (cubic "Cubic Polynomial Kernel") ; Third order polynomial kernel with unit shift - (sigmoid "Sigmoid Kernel") ; Sigmoid kernel with unit shift - (mercersigmoid "Mercer Sigmoid Kernel") ; Mercer variant of Sigmoid kernel with unit shifts - (logarithmic "Logarithmic Kernel") ; Logarithmic kernel with unit shift - (exponential "Exponential Kernel") ; Exponential kernel - (gauss "Gauss Kernel") ; Gaussian kernel with unit covariance - (single "Single Precision Kernel") ; Linear kernel using single precision floating point numbers - (@(x,y) "Custom kernel")) ; Custom kernel function handle + ('training "Training Input" ;; Input used for training trajectories - (training "Training Input" ;; Input used for training trajectories + ('impulse "Delta Impulse") ; (DEFAULT) + ('step "Step Input") ; Constant step input / load vector + ('chirp "Decaying Exponential Chirp") ; Exponential chirp with highest frequency 1/dt + ('sinc "Cardinal Sine") ; Cardinal sine input + ('random "Pseudo-Random Binary")) ; (unseeded) once-sampled scalar {0,1}-sequence - (impulse "Delta Impulse") ; (DEFAULT) - (step "Step Input") ; Constant step input / load vector - (chirp "Decaying Exponential Chirp") ; Exponential chirp with highest frequency 1/dt - (sinc "Cardinal Sine") ; Cardinal sine input - (random "Pseudo-Random Binary")) ; (unseeded) once-sampled scalar {0,1}-sequence + ('weighting "Trajectory Weighting" ;; Scale or normalize discrete trajectory - (weighting "Trajectory Weighting" ;; Scale or normalize discrete trajectory + ('none "No weighting") ; (DEFAULT) + ('linear "Linear Time Weighting") ; Scale each trajectory column by the square-root of its time + ('quadratic "Quadratic Time Weighting") ; Scale each trajectory column by its time + ('state "Per-State Weighting") ; Normalize each trajectory column by its 2-norm + ('scale "Max-Per-Component Weighting") ; Normalize each state by its inf-norm + ('rsqrt "Reciprocal square-root Weighting")) ; Scale each trajectory column by the inverse square-root of its time times pi - (none "No weighting") ; (DEFAULT) - (linear "Linear Time Weighting") ; Scale each trajectory column by the square-root of its time - (quadratic "Quadratic Time Weighting") ; Scale each trajectory column by its time - (state "Per-State Weighting") ; Normalize each trajectory column by its 2-norm - (scale "Max-Per-Component Weighting")) ; Normalize each trajectory row by its inf-norm + ('centering "Trajectory Centering" ;; Center or shift discrete trajectory - (centering "Trajectory Centering" ;; Center or shift discrete trajectory + ('none "No centering") ; (DEFAULT) + ('steady "Steady-State") ; Center (output) trajectory around steady-state (output) + ('final "Final State") ; Center (output) trajectory around last state / output + ('mean "Arithmetic Mean") ; Center (output) trajectory around temporal arithmetic mean + ('rms "Root-Mean-Square") ; Center (output) trajectory around temporal quadratic mean + ('midrange "Mid-Range")) ; Center (output) trajectory around temporal midrange - (none "No centering") ; (DEFAULT) - (steady "Steady-State") ; Center (output) trajectory around steady-state (output) - (final "Final State") ; Center (output) trajectory around last state / output - (mean "Arithmetic Mean") ; Center (output) trajectory around temporal arithmetic mean - (rms "Root-Mean-Square") ; Center (output) trajectory around temporal quadratic mean - (midrange "Mid-Range")) ; Center (output) trajectory around temporal midrange + ('scales "Perturbation Scales" ;; Subdivision of perturbation - (scales "Perturbation Scales" ;; Subdivision of perturbation + ('single "Single scale") ; (DEFAULT) + ('linear "Linear Scaling") ; [0.25,0.50,0.75,1.0] * max_perturbation + ('geometric "Geometric Scaling") ; [0.125,0.25,0.5,1.0] * max_perturbation + ('logarithmic "Logarithmic Scaling") ; [0.001,0.01,0.1,1.0] * max_perturbation + ('sparse "Sparse-Grid Scaling")) ; [0.01,0.50,0.99,1.0] * max_perturbation - (single "Single scale") ; (DEFAULT) - (linear "Linear Scaling") ; [0.25,0.50,0.75,1.0] * max_perturbation - (geometric "Geometric Scaling") ; [0.125,0.25,0.5,1.0] * max_perturbation - (logarithmic "Logarithmic Scaling") ; [0.001,0.01,0.1,1.0] * max_perturbation - (sparse "Sparse-Grid Scaling")) ; [0.01,0.50,0.99,1.0] * max_perturbation + ('rotations "Direction Rotations" ;; Perturbation rotations - (rotations "Direction Rotations" ;; Perturbation rotations + ('posneg "Positive and Negative") ; Unit and negatve unit rotation (DEFAULT) + ('positive "Only Positive")) ; Only unit rotation - (posneg "Positive and Negative") ; Unit and negatve unit rotation (DEFAULT) - (positive "Only Positive")) ; Only unit rotation + ('normalization "Gramian Normalization" ;; Improve numerical behavior by scaling - (normalization "Gramian Normalization" ;; Improve numerical behavior by scaling + ('none "No Normalization") ; (DEFAULT) + ('steady "Steady-State") ; Normalizes vector field argument and value with steady states + ('jacobi "Jacobi Preconditioner")) ; Normalizes vector field argument and value with Gramian diagonal - (none "No Normalization") ; (DEFAULT) - (steady "Steady-State") ; Normalizes vector field argument and value with steady states - (jacobi "Jacobi Preconditioner")) ; Normalizes vector field argument and value with Gramian diagonal + ('stype "State-Space Gramian Variant" ;; Special Gramian variants - (stype "State-Space Gramian Variant" ;; Special Gramian variants + ('standard "Regular") ; (DEFAULT) + ('special "Generic Special") ; Use generic special Gramian variant (for automation purposes) + ('output_controllability "Output Controllability Gramian") ; Use with controllability and sensitivity Gramians + ('average_observability "Average Observability Gramian") ; Use with observablity and identifiability Gramians + ('nonsymmetric_minimality "Non-Symmetric Cross Gramian")) ; Use with (linear) cross and joint Gramians - (standard "Regular") ; (DEFAULT) - (special "Generic Special") ; Use generic special Gramian variant (for automation purposes) - (output_controllability "Output Controllability Gramian") ; Use with controllability and sensitivity Gramians - (average_observability "Average Observability Gramian") ; Use with observablity and identifiability Gramians - (nonsymmetric_minimality "Non-Symmetric Cross Gramian")) ; Use with (linear) cross and joint Gramians + ('extra_input "Control Explicit Observability" ;; Additional input for observability (initial-state perturbation) simulations - (extra_input "Control Explicit Observability" ;; Additional input for observability (initial-state perturbation) simulations + ('none "No Extra Input") ; # (DEFAULT) + ('yes "Use Extra Input")) - (none "No Extra Input") ; # (DEFAULT) - (yes "Use Extra Input")) + ('pcentering "Parameter Centering" ;; Center parameter and generate parameter perturbations - (pcentering "Parameter Centering" ;; Center parameter and generate parameter perturbations + ('none "No Centering") ; Minimal parameter is used as center, scaled linear perturbations (DEFAULT) + ('linear "Linear") ; Center at arithmetic mean, linear scaled perturbations + ('logarithmic "Logarithmic") ; Center at logarithmic mean, logarithmic scaled perturbations (Requires all strictly positive parameter components) + ('nominal "Nominal")) ; Center at nominal parameter, linear scaled perturbations - (none "No Centering") ; Minimal parameter is used as center, scaled linear perturbations (DEFAULT) - (linear "Linear") ; Center at arithmetic mean, linear scaled perturbations - (logarithmic "Logarithmic")) ; Center at logarithmic mean, logarithmic scaled perturbations (Requires all strictly positive parameter components) + ('ptype "Parameter-Space Gramian Variant" ;; Special parameter Gramian variants - (ptype "Parameter-Space Gramian Variant" ;; Special parameter Gramian variants - - (standard "Regular") ; Regular parameter Gramian (DEFAULT) - (special "Generic Special") ; Use generic special Gramian variant (for automation purposes) - (io_sensitivity "Input-Output Sensitivity Gramian") ; Input-output sensitivity Gramian - (coarse_schur "Coarse Schur Complement")) ; Coarse Schur complement computation for empirical identifiability and joint Gramian - - (linearity "System Linearity" ;; Assign system linearity - - (nonlinear "Nonlinear System") ; The system is nonlinear (DEFAULT) - (linear "Linear System"))) ; The system is linear, which requires an adjoint vector field + ('standard "Regular") ; Regular parameter Gramian (DEFAULT) + ('special "Generic Special") ; Use generic special Gramian variant (for automation purposes) + ('io_sensitivity "Input-Output Sensitivity Gramian") ; Input-output sensitivity Gramian + ('coarse_schur "Coarse Schur Complement") ; Coarse Schur complement computation for empirical identifiability and joint Gramian + ('exact_schur "Exact Schur Complement")) ; Exact Schur complement computation for empirical identifiability and joint Gramian + + ('linearity "System Linearity" ;; Assign system linearity + + ('nonlinear "Nonlinear System") ; The system is nonlinear (DEFAULT) + ('linear "Linear System"))) ; The system is linear, which requires an adjoint vector field ) diff --git a/estDemo.m b/estDemo.m index 3cf8647..467d5c4 100644 --- a/estDemo.m +++ b/estDemo.m @@ -1,6 +1,6 @@ function estDemo(t) %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: estDemo - run emgr examples via est @@ -62,6 +62,7 @@ function hnm() config.extra_input = 'yes'; config.skip_x = 3; config.skip_p = 3; + config.ptype = 'exact_schur'; [R,S] = est(sys,task,config); @@ -261,7 +262,7 @@ function lte() sys.N = 256; % Number of states sys.Q = 1; % Number of outputs - A = spdiags(sys.N*ones(sys.N,1)*[1,-1],[-1,0],sys.N,sys.N); % System matrix + A = spdiags(sys.N*ones(sys.N,1)*[1,-1],[-1,0],sys.N,sys.N); % System matrix B = sparse(1,1,sys.N,sys.N,1); % Input matrix C = sparse(1,sys.N,1.0,1,sys.N); % Output matrix @@ -397,7 +398,11 @@ function qso() RP = est(sys,task,config); figure('Name',name,'NumberTitle','off'); - set(gca,'XLim',[0,8],'XTickLabel',{' ','E','L','Q','a','e','\mu','\epsilon',' '},'YScale','log','YGrid','on','NextPlot','add'); + set(gca,'XLim',[0,8],'YLim',[1e-2,1e2], ... + 'XTickLabel',{' ','E','L','Q','a','e','\mu','\epsilon',' '}, ... + 'YScale','log', ... + 'YGrid','on', ... + 'NextPlot','add'); bar([RF,RP]); end diff --git a/estProbe.m b/estProbe.m index 86cdae3..6a2f3fa 100644 --- a/estProbe.m +++ b/estProbe.m @@ -1,6 +1,6 @@ function R = estProbe(sys,m,l) %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: estProbe - factorial test of emgr option flags @@ -36,9 +36,9 @@ T = 0; R = NaN; - for n = {'unit','quadratic','cubic','sigmoid','mercersigmoid','logarithmic','exponential','gauss','dmd','single'} + for n = {'unit','lie','hyp','quadratic','cubic','sigmoid','mercersigmoid','logarithmic','exponential','gauss','dmd','single'} for o = {'impulse','step','sinc','chirp','random'} - for p = {'none','linear','quadratic','state','scale','reciprocal'} + for p = {'none','linear','quadratic','state','scale','rsqrt'} for q = {'none','steady','final','mean','rms','midrange'} for r = {'none','steady','jacobi'} for s = {'standard','special'} diff --git a/estTest.m b/estTest.m index d9eceed..9bda6a5 100644 --- a/estTest.m +++ b/estTest.m @@ -1,11 +1,11 @@ function estTest(t) %%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) -%%% version: 5.9 (2021-01-21) +%%% version: 5.99 (2022-04-13) %%% authors: Christian Himpe (0000-0003-2194-6754) %%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) %%% summary: estTest - test est functionality - rand('seed',1009); % Seed uniform random number generator + rand('seed',1009); % Seed uniform random number generator randn('seed',1009'); M = 4; @@ -99,7 +99,11 @@ function matrix_equation(sys) EOC figure('Name',task.type,'NumberTitle','off'); - set(gca,'YScale','log','YGrid','on','XLim',[1,2],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'YGrid','on', ... + 'XLim',[1,2], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c,'LineWidth',3),err); legend(name(:)); end @@ -141,7 +145,11 @@ function singular_values(sys) MORscore = ms figure('Name',task.type,'NumberTitle','off'); - set(gca,'YScale','log','YGrid','on','XLim',[1,sys.N],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'YGrid','on', ... + 'XLim',[1,sys.N], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c./max(c),'LineWidth',3),sv); legend(name(:)); end @@ -200,7 +208,13 @@ function model_reduction(sys) for k = 1:numel(method) for l = 1:4 subplot(4,numel(method),(l-1)*numel(method)+k); - set(gca,'YScale','log','Ytick',[1e-16,1e-8,1e-0],'YGrid','on','XLim',[1,sys.N-1],'YLim',[1e-16,1.1],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'Ytick',[1e-16,1e-8,1e-0], ... + 'YGrid','on', ... + 'XLim',[1,sys.N-1], ... + 'YLim',[1e-16,1.1], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c{l+2},'LineWidth',3),mr(k,:,:)); if isequal(k,1), ylabel(yl{l}); end%if if isequal(l,1), title(method{k},'Interpreter','none'); end%if @@ -243,11 +257,18 @@ function parameter_reduction(sys) yl = {'L_1','L_2','L_\infty','L_0'}; for l = 1:4 subplot(4,1,l); - set(gca,'YScale','log','Ytick',[1e-16,1e-8,1],'YGrid','on','XLim',[1,sys.N-1],'YLim',[1e-16,1.1],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'Ytick',[1e-16,1e-8,1], ... + 'YGrid','on', ... + 'XLim',[1,sys.N-1], ... + 'YLim',[1e-16,1.1], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); for k = 1:numel(method) plot(pr{k}{l+2},'LineWidth',3); end%for ylabel(yl{l}); + legend(method(:)); end%for end @@ -293,7 +314,8 @@ function combined_reduction(sys) h = surf(cr{m}{1},cr{m}{2},cr{m}{5}); xlabel('x'); ylabel('p'); - set(gca,'ZScale','log','CLim',log10(get(gca,'ZLim'))); + set(gca,'ZScale','log', ... + 'CLim',log10(get(gca,'ZLim'))); set(h,'CData',log10(get(h,'CData'))); view(135,15); end%for @@ -359,7 +381,9 @@ function state_sensitivity(sys) for k = 1:numel(method) for l = 1:numel(linearity) - disp(['Testing: ',method{k},' (',linearity{l},')']); + name{k,l} = [method{k},' (',linearity{l},')']; + + disp(['Testing: ',name{k,l}]); ds{l,k} = est(sys,setfield(task,'method',method{k}), ... setfield(config,'linearity',linearity{l})); @@ -369,8 +393,11 @@ function state_sensitivity(sys) figure('Name',task.type,'NumberTitle','off'); for k = 1:numel(linearity) subplot(1,numel(linearity),k); - set(gca,'XLim',[1,sys.N],'NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,sys.N], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c,'LineWidth',3), ds(k,:)); + legend(name(:,k)); end%for end @@ -401,8 +428,13 @@ function parameter_sensitivity(sys) end%for figure('Name',task.type,'NumberTitle','off'); - set(gca,'YScale','log','XLim',[1,sys.N],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'YLim',[1e-3,1e2], ... + 'XLim',[1,sys.N], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c,'LineWidth',3),ps); + legend(method(:)); end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -429,8 +461,12 @@ function parameter_identifiability(sys) end%for figure('Name',task.type,'NumberTitle','off'); - set(gca,'YScale','log','XLim',[1,sys.N],'NextPlot','add','ColorOrder',lines12); + set(gca,'YScale','log', ... + 'XLim',[1,sys.N], ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c./max(c),'LineWidth',3),pj); + legend(method(:)); end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -456,11 +492,19 @@ function uncertainty_quantification(sys) figure('Name',task.type,'NumberTitle','off'); subplot(1,2,1); - set(gca,'XLim',[1,sys.N],'YScale','log','NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,sys.N], ... + 'YScale','log', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); plot(uq{1}./max(uq{1}),'LineWidth',3); + legend(method{1}); subplot(1,2,2); - set(gca,'XLim',[1,sys.Q],'YScale','log','NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,sys.Q], ... + 'YScale','log', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); plot(uq{2}./max(uq{2}),'LineWidth',3); + legend(method{2}); end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -489,8 +533,12 @@ function nonlinearity_quantification(sys) end%for figure('Name',task.type,'NumberTitle','off'); - set(gca,'XLim',[1,10],'YScale','log','NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,10], ... + 'YScale','log', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c,'LineWidth',3),nq); + legend(method(:)); end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -507,6 +555,7 @@ function gramian_index(sys) 'operator_norm', ... 'sigma_max', ... 'log_det', ... + 'entropy', ... 'storage_efficiency', ... 'unobservability_index', ... 'performance_index'}; @@ -538,7 +587,10 @@ function gramian_index(sys) for l = 1:numel(variant) subplot(1,numel(variant),l); - set(gca,'XLim',[1,sys.N],'YScale','log','NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,sys.N], ... + 'YScale','log', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c./max(c),'LineWidth',3),gi(:,l)); title(variant{l},'Interpreter','None'); legend(method,'Location','SouthOutside','Interpreter','None'); @@ -633,7 +685,10 @@ function system_norm(sys) end%for figure('Name',task.type,'NumberTitle','off'); - set(gca,'XLim',[1,sys.N],'YScale','log','NextPlot','add','ColorOrder',lines12); + set(gca,'XLim',[1,sys.N], ... + 'YScale','log', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c./max(c),'LineWidth',3),sn); title('Norms','Interpreter','None'); legend(method,'Location','SouthOutside','Interpreter','None'); @@ -662,7 +717,9 @@ function tau_function(sys) end%for figure('Name',task.type,'NumberTitle','off'); - set(gca,'YGrid','on','NextPlot','add','ColorOrder',lines12); + set(gca,'YGrid','on', ... + 'NextPlot','add', ... + 'ColorOrder',lines12); cellfun(@(c) plot(c,'LineWidth',3),au); legend(name(:)); end diff --git a/py/RUNME.py b/py/RUNME.py index 405a544..161de17 100755 --- a/py/RUNME.py +++ b/py/RUNME.py @@ -2,7 +2,7 @@ """ project: emgr ( https://gramian.de ) - version: 5.9.py (2021-01-21) + version: 5.99.py (2022-04-13) authors: Christian Himpe (0000-0003-2194-6754) license: BSD-2-Clause License (opensource.org/licenses/BSD-2-Clause) summary: RUNME (Minimal emgr test script) @@ -19,38 +19,44 @@ B = np.array([[0.0,1.0,0.0,1.0]]).T C = np.array([[0.0,0.0,1.0,1.0]]) -def f(x,u,p,t): return A.dot(x) + B.dot(u) +P = np.zeros((4,1)) +Q = np.array([[0.01,0.25],[0.01,0.5],[0.01,0.75],[0.01,1.0]]) + +def f(x,u,p,t): return A.dot(x) + B.dot(u) + p def g(x,u,p,t): return C.dot(x) def h(x,u,p,t): return A.T.dot(x) + C.T.dot(u) +s = (1,4,1) +t = (0.01,1.0) + # (Empircal) Controllability Gramian -WC = emgr(f,g,(1,4,1),(0.01,1.0),"c") +WC = emgr(f,g,s,t,"c",P) print(WC) # (Empirical) Observability Gramian -WO = emgr(f,g,(1,4,1),(0.01,1.0),"o") +WO = emgr(f,g,s,t,"o",P) print(WO) # (Empirical) Cross Gramian -WX = emgr(f,g,(1,4,1),(0.01,1.0),"x") +WX = emgr(f,g,s,t,"x",P) print(WX) # (Empirical) Linear Cross Gramian -WY = emgr(f,h,(1,4,1),(0.01,1.0),"y") +WY = emgr(f,h,s,t,"y",P) print(WY) -Q = np.array([[0.0,0.25],[0.0,0.5],[0.0,0.75],[0.0,1.0]]) - -def f(x,u,p,t): return A.dot(x) + B.dot(u) + p - # (Empircal) Controllability Gramian -WC,WS = emgr(f,g,(1,4,1),(0.01,1.0),"s",Q) +WC,WS = emgr(f,g,s,t,"s",Q) +print(WC) print(WS) # (Empirical) Observability Gramian -WO,WI = emgr(f,g,(1,4,1),(0.01,1.0),"i",Q) +WO,WI = emgr(f,g,s,t,"i",Q) +print(WO) print(WI) # (Empirical) Cross Gramian -WX,WJ = emgr(f,g,(1,4,1),(0.01,1.0),"j",Q) +WX,WJ = emgr(f,g,s,t,"j",Q) +print(WX) print(WJ) + diff --git a/py/emgr.py b/py/emgr.py index edb72c6..4d0df1f 100644 --- a/py/emgr.py +++ b/py/emgr.py @@ -3,7 +3,7 @@ ================================== project: emgr ( https://gramian.de ) - version: 5.9.py (2021-01-21) + version: 5.99.py (2022-04-13) authors: Christian Himpe (0000-0003-2194-6754) license: BSD-2-Clause License (opensource.org/licenses/BSD-2-Clause) summary: Empirical system Gramians for (nonlinear) input-output systems. @@ -18,6 +18,11 @@ Data-driven analysis of input-output coherence and system-gramian-based nonlinear model order reduction. Compatible with PYTHON3. +BRIEF: +------ + + Unsupervised learning of I/O system properties for data-driven control. + ALGORITHM: ---------- @@ -49,7 +54,7 @@ ------------------- pr {matrix|0} parameter vector(s), each column is one parameter sample - nf {vector|0} option flags, thirteen component vector, default all zero: + nf {tuple|0} option flags, thirteen component vector, default all zero: * centering: none(0), steady(1), last(2), mean(3), rms(4), midrange(5) * input scales: single(0), linear(1), geometric(2), log(3), sparse(4) * state scales: single(0), linear(1), geometric(2), log(3), sparse(4) @@ -61,23 +66,23 @@ * observability gramian type (only: Wo, Wi): regular(0), averaged(1) * cross gramian type (only: Wx, Wy, Wj): regular(0), non-symmetric(1) * extra input (only: Wo, Wx, Ws, Wi, Wj): no(0), yes(1) - * parameter centering (only: Ws, Wi, Wj): none(0), linear(1), log(2) + * parameter centering (only: Ws, Wi, Wj): none(0), lin(1), log(2), nom(3) * parameter gramian variant: * averaging type (only: Ws): input-state(0), input-output(1) * Schur-complement (only: Wi, Wj): approx(0), coarse(1) * cross gramian partition size (only: Wx, Wj): full(0), partitioned(0) - * weighting: none(0), linear(1), squared(2), state(3), scale(4) + * weighting: none(0), linear(1), squared(2), state(3), scale(4), rsqrt(5) ut {handle|'i'} input function: u_t = ut(t) or single character string: * "i" delta impulse input * "s" step input / load vector / source term * "h" havercosine decaying exponential chirp input * "a" sinc (cardinal sine) input * "r" pseudo-random binary input - us {vector|0} steady-state input (1 or M rows) - xs {vector|0} steady-state and nominal initial state x_0 (1 or N rows) - um {matrix|1} input scales (1 or M rows) - xm {matrix|1} initial-state scales (1 or N rows) + us {vector|0} steady-state input (1 or #inputs rows) + xs {vector|0} steady-state and nominal initial state x_0 (1 or #states rows) + um {matrix|1} input scales (1 or #inputs rows) + xm {matrix|1} initial-state scales (1 or #states rows) dp {handle|@mtimes} inner product or kernel: xy = dp(x,y) RETURNS: @@ -89,8 +94,8 @@ CITE AS: -------- - C. Himpe (2021). emgr - EMpirical GRamian Framework (Version 5.9) - [Software]. Available from https://gramian.de . doi:10.5281/zenodo.4454679 + C. Himpe (2022). emgr - EMpirical GRamian Framework (Version 5.99) + [Software]. Available from https://gramian.de . doi:10.5281/zenodo.6457616 KEYWORDS: --------- @@ -111,9 +116,9 @@ import math import numpy as np -__version__ = "5.9" -__date__ = "2021-01-21" -__copyright__ = "Copyright (C) 2021 Christian Himpe" +__version__ = "5.99" +__date__ = "2022-04-13" +__copyright__ = "Copyright (C) 2022 Christian Himpe" __author__ = "Christian Himpe" __license__ = "BSD 2-Clause" @@ -128,6 +133,8 @@ def emgr(f, g=None, s=None, t=None, w=None, pr=0, nf=0, ut="i", us=0.0, xs=0.0, if f == "version": return __version__ + fState = f + # Default Arguments if isinstance(pr, (int, float)) or np.ndim(pr) == 1: pr = np.reshape(pr, (-1, 1)) @@ -137,183 +144,191 @@ def emgr(f, g=None, s=None, t=None, w=None, pr=0, nf=0, ut="i", us=0.0, xs=0.0, ############################################################################### # System Dimensions - M = int(s[0]) # Number of inputs - N = int(s[1]) # Number of states - Q = int(s[2]) # Number of outputs - P = pr.shape[0] # Dimension of parameter - K = pr.shape[1] # Number of parameter-sets + nInputs = int(s[0]) # Number of inputs + nStates = int(s[1]) # Number of states + nOutputs = int(s[2]) # Number of outputs - # Time Discretization - dt = t[0] # Time-step width - Tf = t[1] # Time horizon - nt = int(math.floor(Tf / dt) + 1) # Number of time-steps + # Parameter Dimensions + nParams = pr.shape[0] # Dimension of parameter + nParamSamples = pr.shape[1] # Number of parameter-sets - # Force lower-case Gramian type - w = w[0].lower() + # Time Discretization + tStep = t[0] # Time-step width + tFinal = t[1] # Time horizon + nSteps = int(math.floor(tFinal / tStep) + 1) # Number of time-steps - # Lazy Output Functional - if isinstance(g, int) and g == 1: - g = ident - Q = N + # Gramian Type + gramianType = w[0].lower() - # Pad Flag Vector + # Flag Vector if nf == 0: - nf = [0] + flags = [0] + else: + flags = nf - if len(nf) < 13: - nf = nf + [0] * (13 - len(nf)) + if len(flags) < 13: + flags = flags + [0] * (13 - len(flags)) - # Built-in input functions + # Built-in Input Functions if isinstance(ut, str): - if ut.lower() == "s": # Step Input - def ut(t): - return 1 - elif ut.lower() == "h": # Havercosine Chirp Input - a0 = math.pi / (2.0 * dt) * Tf / math.log(4.0 * (dt / Tf)) - b0 = (4.0 * (dt / Tf)) ** (1.0 / Tf) + a0 = math.pi / (2.0 * tStep) * tFinal / math.log(4.0 * (tStep / tFinal)) + b0 = (4.0 * (tStep / tFinal)) ** (1.0 / tFinal) + + if ut.lower() == "i": # Delta Impulse Input + def fExcite(t): + return float(t <= tStep) / tStep - def ut(t): + elif ut.lower() == "s": # Step Input + def fExcite(t): + return 1.0 + + elif ut.lower() == "h": # Havercosine Chirp Input + def fExcite(t): return 0.5 * math.cos(a0 * (b0 ** t - 1.0)) + 0.5 elif ut.lower() == "a": # Sinc Input - def ut(t): - return math.sin(t / dt) / ((t / dt) + float(t == 0)) + def fExcite(t): + return math.sin(t / tStep) / ((t / tStep) + float(t == 0)) elif ut.lower() == "r": # Pseudo-Random Binary Input - rt = np.random.randint(0, 2, size=nt) + def fExcite(t): + return np.random.randint(0, 1, 1) - def ut(t): - return rt[int(math.floor(t / dt))] - - else: # Delta Impulse Input - def ut(t): - return float(t <= dt) / dt - - # Lazy Optional Arguments - if isinstance(us, (int, float)): us = np.full(M, us) - if isinstance(xs, (int, float)): xs = np.full(N, xs) - if isinstance(um, (int, float)): um = np.full(M, um) - if isinstance(xm, (int, float)): xm = np.full(N, xm) + else: + assert False, "emgr: unknown input ut!" + else: + fExcite = ut ############################################################################### # CONFIGURATION ############################################################################### + # Output Function + if (isinstance(g, int) and g == 1) or ((gramianType == "c") and not flags[6]) or (gramianType == "y"): + fOutput = ident + fAdjoint = g + else: + fOutput = g + # Trajectory Weighting - if nf[12] == 1: # Linear Time-Weighting - def wei(m): - return np.sqrt(np.linspace(0, Tf, nt)) + tInstances = np.linspace(0, tFinal, nSteps)[np.newaxis, :] + tInstances[0, 0] = 0.5 * tStep + if flags[12] == 1: # Linear Time-Weighting + def fWeight(traj): + return traj * np.sqrt(tInstances) - elif nf[12] == 2: # Quadratic Time-Weighting - def wei(m): - return np.linspace(0, Tf, nt) * math.sqrt(0.5) + elif flags[12] == 2: # Quadratic Time-Weighting + def fWeight(traj): + return traj * (tInstances / math.sqrt(2.0)) - elif nf[12] == 3: # State-Weighting - def wei(m): - return 1.0 / np.maximum(math.sqrt(np.spacing(1)), np.linalg.norm(m, 2, axis=0)) + elif flags[12] == 3: # State-Weighting + def fWeight(traj): + return traj / np.maximum(math.sqrt(np.spacing(1)), np.linalg.norm(traj, 2, axis=0)) - elif nf[12] == 4: # Scale-Weighting - def wei(m): - return 1.0 / np.maximum(math.sqrt(np.spacing(1)), np.linalg.norm(m, np.inf, axis=1)[:, np.newaxis]) + elif flags[12] == 4: # Scale-Weighting + def fWeight(traj): + return traj / np.maximum(math.sqrt(np.spacing(1)), np.linalg.norm(traj, np.inf, axis=1)[:, np.newaxis]) + + elif flags[12] == 5: # Reciprocal Square-Root Time-Weighting + def fWeight(traj): + return traj / (math.pi * tInstances) ** 0.25 else: # None - def wei(m): - return 1.0 + def fWeight(traj): + return traj # Trajectory Centering - if nf[0] == 1: # Steady-State / Output - def avg(m, s): - return s.reshape(-1, 1) + if flags[0] == 1: # Steady-State / Output + def fCenter(traj, xs): + return traj - xs.reshape(-1, 1) - elif nf[0] == 2: # Final State / Output - def avg(m, s): - return m[:, -1].reshape(-1, 1) + elif flags[0] == 2: # Final State / Output + def fCenter(traj, xs): + return traj - traj[:, -1].reshape(-1, 1) - elif nf[0] == 3: # Temporal Mean State / Output - def avg(m, s): - return np.mean(m, axis=1).reshape(-1, 1) + elif flags[0] == 3: # Temporal Mean State / Output + def fCenter(traj, xs): + return traj - np.mean(traj, axis=1).reshape(-1, 1) - elif nf[0] == 4: # Temporal Root-Mean-Square / Output - def avg(m, s): - return np.sqrt(np.mean(m * m, axis=1)).reshape(-1, 1) + elif flags[0] == 4: # Temporal Root-Mean-Square / Output + def fCenter(traj, xs): + return traj - np.sqrt(np.mean(traj * traj, axis=1)).reshape(-1, 1) - elif nf[0] == 5: # Temporal Mid-range of State / Output - def avg(m, s): - return 0.5 * (np.amax(m, axis=1) + np.amin(m, axis=1)).reshape(-1, 1) + elif flags[0] == 5: # Temporal Mid-range of State / Output + def fCenter(traj, xs): + return traj - 0.5 * (np.amax(traj, axis=1) + np.amin(traj, axis=1)).reshape(-1, 1) else: # None - def avg(m, s): - return 0.0 + def fCenter(traj, xs): + return traj - # Gramian Normalization - if nf[5] and w in {"c", "o", "x", "y"}: + # Steady State + vSteadyInput = np.full((nInputs, 1), us) if np.isscalar(us) else us + vSteadyState = np.full((nStates, 1), xs) if np.isscalar(xs) else xs - nf[5] = 0 - TX = xs # Steady-state preconditioner + # Gramian Normalization + if flags[5] in {1, 2} and w in {"c", "o", "x", "y"}: - if nf[5] == 2: # Jacobi-type preconditioner - NF = nf + if flags[5] == 2: # Jacobi-type preconditioner + NF = list(flags) NF[5] = 0 if w == "c": NF[6] = 0 - PR = np.mean(pr, axis=1) def DP(x, y): - return np.sum(x * y.T, 1) # Diagonal-only kernel - TX = np.sqrt(np.fabs(emgr(f, g, s, t, w, PR, NF, ut, us, xs, um, xm, DP))) + return np.sum(x[:nStates, :] * y[:, :nStates].T, 1) # Diagonal-only kernel + + TX = np.sqrt(np.abs(emgr(f, g, s, t, w, np.mean(pr, axis=1), NF, ut, us, xs, um, xm, DP)))[:, np.newaxis] + + if flags[5] == 1: # Steady-state preconditioner + TX = vSteadyState TX[np.fabs(TX) < np.sqrt(np.spacing(1))] = 1.0 - tx = TX if w == "y" else 1.0 + vSteadyState = vSteadyState / TX - def deco(f, g): - def F(x, u, p, t): - return f(TX * x, u, p, t) / TX + def fState(x, u, p, t): + return f(TX * x, u, p, t) / TX - def G(x, u, p, t): - return g(TX * x, u, p, t) / tx + def fAdjoint(x, u, p, t): + return g(TX * x, u, p, t) / TX - return F, G + if fOutput == ident: - f, g = deco(f, g) + def fOutput(x, u, p, t): + return ident(TX * x, u, p, t) + else: - xs = xs / TX + def fOutput(x, u, p, t): + return g(TX * x, u, p, t) - # State gramian variant setup - if nf[6]: - G = g # Output Controllability Gramian - R = 1 # Average Observability Cache Size + # Output Averaging + nPages = 1 if flags[6] != 0 else nOutputs - def oavg(y): # Average Observability Gramian - return np.sum(y, axis=1) + # Extra Input (for control explicit observability) + if flags[7] != 0: + def fSteady(t): + return vSteadyInput + fExcite(t) - S = 0 # Non-Symmetric (Linear) Cross Gramian else: - G = ident # Regular Controllability Gramian - R = Q # Regular Observability Cache Size + def fSteady(t): + return vSteadyInput - def oavg(y): - return y.flatten(0) # Regular Observability Gramian + # Perturbation Scales + vInputMax = np.full((nInputs, 1), um) if np.isscalar(um) else um + vStateMax = np.full((nStates, 1), xm) if np.isscalar(xm) else xm + vOutputMax = np.full((nOutputs, 1), xm) if np.isscalar(xm) else xm - S = 1 # Regular (Linear) Cross Gramian + mInputScales = (vInputMax * scales(flags[1], flags[3])) if vInputMax.shape[1] == 1 else vInputMax + mStateScales = (vStateMax * scales(flags[2], flags[4])) if vStateMax.shape[1] == 1 else vStateMax + mOutputScales = (vOutputMax * scales(flags[1], flags[3])) if vOutputMax.shape[1] == 1 else vOutputMax - # Extra input - if nf[7]: - def up(t): - return us + ut(t) - else: - def up(t): - return us - - # Scale Sampling - if um.ndim == 1: um = np.outer(um, scales(nf[1], nf[3])) - if xm.ndim == 1: vm = np.outer(xm[0:Q], scales(nf[1], nf[3])) - if xm.ndim == 1: xm = np.outer(xm, scales(nf[2], nf[4])) + nTotalStates = mStateScales.shape[0] - A = xm.shape[0] # Number of total states (regular and augmented) - C = um.shape[1] # Number of input scales sets - D = xm.shape[1] # Number of state scales sets + nInputScales = mInputScales.shape[1] + nStateScales = mStateScales.shape[1] + nOutputScales = mOutputScales.shape[1] ############################################################################### # EMPIRICAL SYSTEM GRAMIAN COMPUTATION @@ -324,7 +339,8 @@ def up(t): # Common Layout: # For each {parameter, scale, input/state/parameter component}: # Perturb, simulate, weight, center, normalize, accumulate - # Parameter gramians call state gramians + # Output and adjoint trajectories are cached to prevent recomputation + # Parameter gramians "s", "i", "j" call state gramians "c", "o", "x" ############################################################################### # EMPIRICAL CONTROLLABILITY GRAMIAN @@ -332,22 +348,22 @@ def up(t): if w == "c": - for k in range(K): - for c in range(C): - for m in range(M): - if um[m, c] != 0: - em = np.zeros(M + P) - em[m] = um[m, c] - - def umc(t): - return up(t) + ut(t) * em[0:M] - pmc = pr[:, k] + em[M:M + P] - x = ODE(f, G, t, xs, umc, pmc) - x *= wei(x) - x -= avg(x, G(xs, us, pmc, 0)) - x /= um[m, c] - W += dp(x, x.T) - W *= dt / (C * K) + for k in range(nParamSamples): + vParam = pr[:, [k]] + vSteadyOutput = fOutput(vSteadyState, vSteadyInput, vParam, 0) + for c in range(nInputScales): + for m in range(nInputs): + sPerturb = mInputScales[m, c] + if sPerturb != 0.0: + vUnit = np.zeros(nInputs) + vUnit[m] = sPerturb + + def fInput(t): + return vSteadyInput + vUnit * fExcite(t) + + mTraj = fWeight(fCenter(ODE(fState, fOutput, t, vSteadyState, fInput, vParam), vSteadyOutput)) / sPerturb + W += dp(mTraj, mTraj.T) + W *= tStep / (nInputScales * nParamSamples) return W ############################################################################### @@ -356,23 +372,28 @@ def umc(t): if w == "o": - o = np.zeros((R * nt, A)) # Pre-allocate observability matrix - for k in range(K): - for d in range(D): - for n in range(A): - if xm[n, d] != 0: - en = np.zeros(N + P) - en[n] = xm[n, d] - xnd = xs + en[0:N] - pnd = pr[:, k] + en[N:N + P] - y = ODE(f, g, t, xnd, up, pnd) - y *= wei(y) - y -= avg(y, g(xs, us, pnd, 0)) - y /= xm[n, d] - o[:, n] = oavg(y.T) - W += dp(o.T, o) - W *= dt / (D * K) - return W + obsCache = np.zeros((nPages * nSteps, nTotalStates)) + for k in range(nParamSamples): + vParam = pr[:, [k]] + for d in range(nStateScales): + for n in range(nTotalStates): + sPerturb = mStateScales[n, d] + if sPerturb != 0.0: + vUnit = np.zeros((nTotalStates, 1)) + vUnit[n] = sPerturb + vInit = vSteadyState + vUnit[:nStates] + vParamInit = np.copy(vParam) + if nTotalStates > nStates: + vParamInit += vUnit[nStates:] + vSteadyOutput = fOutput(vSteadyState, vSteadyInput, vParamInit, 0) + mTraj = fWeight(fCenter(ODE(fState, fOutput, t, vInit, fSteady, vParamInit), vSteadyOutput)) / sPerturb + if flags[6] != 0: + obsCache[:, n] = np.sum(mTraj, axis=0) + else: + obsCache[:, n] = mTraj.flatten(order='F') + W += dp(obsCache.T, obsCache) + W *= tStep / (nStateScales * nParamSamples) + return W ############################################################################### # EMPIRICAL CROSS GRAMIAN @@ -380,52 +401,57 @@ def umc(t): if w == "x": - assert M == Q or nf[6], "emgr: non-square system!" + assert nInputs == nOutputs or nf[6], "emgr: non-square system!" - i0 = 0 - i1 = A + colFirst = 0 # Start partition column index + colLast = nTotalStates # Final partition column index # Partitioned cross gramian - if nf[10] > 0: - sp = int(round(nf[10])) # Partition size - ip = int(round(nf[11])) # Partition index - i0 += ip * sp # Start index - i1 = min(i0 + sp, N) # End index - if i0 > N: - i0 -= math.ceil(N / sp) * sp - N - i1 = min(i0 + sp, A) - - if ip < 0 or i0 >= i1 or i0 < 0: + if flags[10] > 0: + parSize = int(round(nf[10])) # Partition size + parIndex = int(round(nf[11])) # Partition index + colFirst += (parIndex - 1) * parSize # Start index + colLast = min(colFirst + (parSize - 1), nStates) + if colFirst > nStates: + colFirst -= (math.ceil(nStates / parSize) * parSize - nStates) + colLast = min(colFirst + parSize - 1, nTotalStates) + + if parIndex < 0 or colFirst >= colLast or colFirst < 0: return 0 - o = np.zeros((R * nt, i1 - i0)) # Pre-allocate observability cache - for k in range(K): - for d in range(D): - for n in range(i1 - i0): - if xm[n, d] != 0: - en = np.zeros(N + P) - en[i0 + n] = xm[i0 + n, d] - xnd = xs + en[0:N] - pnd = pr[:, k] + en[N:N + P] - y = ODE(f, g, t, xnd, up, pnd) - y *= wei(y) - y -= avg(y, g(xs, us, pnd, 0)) - y /= xm[i0 + n, d] - o[:, n] = oavg(y.T) - for c in range(C): - for m in range(M): - if um[m, c] != 0: - em = np.zeros(M) - em[m] = um[m, c] - - def umc(t): - return us + ut(t) * em - x = ODE(f, ident, t, xs, umc, pr[:, k]) - x *= wei(x) - x -= avg(x, xs) - x /= um[m, c] - W += dp(x, o[(nt * (S * (m - 1))):(nt * (S * m) + nt), :]) - W *= dt / (C * D * K) + obsCache = np.zeros((nSteps * nPages, colLast - colFirst)) + + for k in range(nParamSamples): + vParam = pr[:, [k]] + for d in range(nStateScales): + for n in range(colLast - colFirst): + sPerturb = mStateScales[colFirst + n, d] + if sPerturb != 0.0: + vUnit = np.zeros((nTotalStates, 1)) + vUnit[colFirst + n] = sPerturb + vInit = vSteadyState + vUnit[:nStates] + vParamInit = np.copy(vParam) + if nTotalStates > nStates: + vParamInit += vUnit[nStates:] + vSteadyOutput = fOutput(vSteadyState, vSteadyInput, vParamInit, 0) + mTraj = fWeight(fCenter(ODE(fState, fOutput, t, vInit, fSteady, vParamInit), vSteadyInput)) / sPerturb + if flags[6] != 0: + obsCache[:, n] = np.sum(mTraj, axis=0).T + else: + obsCache[:, n] = mTraj.T.flatten(0) + for c in range(nInputScales): + for m in range(nInputs): + sPerturb = mInputScales[m, c] + if sPerturb != 0.0: + vUnit = np.zeros((nInputs, 1)) + vUnit[m] = sPerturb + + def fInput(t): + return vSteadyInput + vUnit * fExcite(t) + mTraj = fWeight(fCenter(ODE(fState, ident, t, vSteadyState, fInput, vParam), vSteadyInput)) / sPerturb + nBlock = 0 if flags[6] else m * nSteps + W += dp(mTraj, obsCache[nBlock:nBlock + nSteps, :]) + W *= tStep / (nInputScales * nStateScales * nParamSamples) return W ############################################################################### @@ -434,39 +460,37 @@ def umc(t): if w == "y": - assert M == Q or nf[6], "emgr: non-square system!" - assert C == vm.shape[1], "emgr: scale count mismatch!" - - a = np.zeros((N * nt, Q)) # Pre-allocate adjoint cache - for k in range(K): - for c in range(C): - for q in range(Q): - if vm[q, c] != 0: - em = np.zeros(Q) - em[q] = vm[q, c] - - def vqc(t): - return us + ut(t) * em - z = ODE(g, ident, t, xs, vqc, pr[:, k]) - z *= wei(z) - z -= avg(z, xs) - z /= vm[q, c] - a[:, q] = z.flatten(0) - if nf[6]: # Non-symmetric cross gramian - a[:, 0] = np.sum(a, axis=1) - for m in range(M): - if um[m, c] != 0: - em = np.zeros(M) - em[m] = um[m, c] - - def umc(t): - return us + ut(t) * em - x = ODE(f, ident, t, xs, umc, pr[:, k]) - x *= wei(x) - x -= avg(x, xs) - x /= um[m, c] - W += dp(x, np.reshape(a[:, S * m], (N, nt)).T) - W *= dt / (C * K) + assert nInputs == nOutputs or nf[6], "emgr: non-square system!" + assert nInputScales == nOutputScales, "emgr: scale count mismatch!" + + adjCache = np.zeros((nSteps, nStates, nPages)) + + for k in range(nParamSamples): + vParam = pr[:, [k]] + for c in range(nInputScales): + for q in range(nOutputs): + sPerturb = mOutputScales[q, c] + if sPerturb != 0.0: + vUnit = np.zeros((nOutputs, 1)) + vUnit[q] = sPerturb + + def fInput(t): + return vSteadyInput + vUnit * fExcite(t) + mTraj = fWeight(fCenter(ODE(fAdjoint, ident, t, vSteadyState, fInput, vParam), vSteadyInput)) / sPerturb + adjCache[:, :, q] = mTraj.T + if flags[6] != 0: + adjCache[:, :, 0] = np.sum(adjCache, axis=2) + for m in range(nInputs): + sPerturb = mInputScales[m, c] + if sPerturb != 0.0: + vUnit = np.zeros((nInputs, 1)) + vUnit[m] = sPerturb + + def fInput(t): + return vSteadyInput + vUnit * fExcite(t) + mTraj = fWeight(fCenter(ODE(fState, ident, t, vSteadyState, fInput, vParam), vSteadyInput)) / sPerturb + W += dp(mTraj, adjCache[:, :, 0 if flags[6] != 0 else m]) + W *= tStep / (nInputScales * nParamSamples) return W ############################################################################### @@ -475,29 +499,30 @@ def umc(t): if w == "s": - # Empirical Controllability Gramian - pr, pm = pscales(pr, nf[8], C) - WC = emgr(f, g, s, t, "c", pr, nf, ut, us, xs, um, xm, dp) + # Controllability Gramian + pr, mParamScales = paramScales(pr, flags[8], nInputScales) + WC = emgr(f, g, s, t, "c", pr, flags, ut, us, xs, um, xm, dp) - if not nf[9]: # Input-state sensitivity gramian + if not flags[9]: # Input-state sensitivity gramian def DP(x, y): - return np.sum(x * y.T) # Trace pseudo-kernel - else: # Input-output sensitivity gramian + return np.sum(x * y.T) # Trace pseudo-kernel + else: # Input-output sensitivity gramian def DP(x, y): - return np.sum(np.reshape(y, (R, -1))) # Custom pseudo-kernel - - Y = emgr(f, g, s, t, "o", pr, nf, ut, us, xs, um, xm, DP) + return y # Custom pseudo-kernel + flags[6] = 1 + Y = emgr(f, g, s, t, "o", pr, flags, ut, us, xs, um, xm, DP) + flags[6] = 0 def DP(x, y): - return np.fabs(np.sum(y * Y)) # Custom pseudo-kernel + return np.abs(np.sum(y * Y)) # Custom pseudo-kernel # (Diagonal) Sensitivity Gramian - WS = np.zeros((P, P)) + WS = np.zeros((nParams, 1)) - for p in range(P): - pp = np.tile(pr, (1, pm.shape[1])) - pp[p, :] = pp[p, :] + pm[p, :] - WS[p, p] = emgr(f, g, s, t, "c", pp, nf, ut, us, xs, um, xm, DP) + for p in range(nParams): + paramSamples = np.tile(pr, (1, mParamScales.shape[1])) + paramSamples[p, :] += mParamScales[p, :] + WS[p] = emgr(f, g, s, t, "c", paramSamples, flags, ut, us, xs, um, xm, DP) return WC, WS @@ -508,17 +533,22 @@ def DP(x, y): if w == "i": # Augmented Observability Gramian - pr, pm = pscales(pr, nf[8], D) - V = emgr(f, g, s, t, "o", pr, nf, ut, us, xs, um, np.vstack((xm, pm)), dp) + pr, mParamScales = paramScales(pr, flags[8], nStateScales) + V = emgr(f, g, s, t, "o", pr, flags, ut, us, xs, um, np.vstack((mStateScales, mParamScales)), dp) + + # Return augmented observability gramian + if flags[10] != 0: + return V - WO = V[0:N, 0:N] # Observability Gramian - WM = V[0:N, N:N + P] # Mixed Block + WO = V[:nStates, :nStates] # Observability Gramian + WM = V[:nStates, nStates:] # Mixed Block + WI = V[nStates:, nStates:] # Parameter Gramian # Identifiability Gramian - if not nf[9]: # Schur-complement via approximate inverse - WI = V[N:N + P, N:N + P] - WM.T.dot(ainv(WO)).dot(WM) - else: # Coarse Schur-complement via zero - WI = V[N:N + P, N:N + P] + if flags[9] == 2: # Exact Schur-complement via pseudo-inverse + WI -= (WM.T).dot(np.linalg.pinv(WO)).dot(WM) + elif flags[9] == 0: # Approximate Schur-complement via approximate inverse + WI -= (WM.T).dot(ainv(WO)).dot(WM) return WO, WI @@ -529,89 +559,109 @@ def DP(x, y): if w == "j": # Empirical Joint Gramian - pr, pm = pscales(pr, nf[8], D) - V = emgr(f, g, s, t, "x", pr, nf, ut, us, xs, um, np.vstack((xm, pm)), dp) + pr, mParamScales = paramScales(pr, flags[8], nStateScales) + V = emgr(f, g, s, t, "x", pr, flags, ut, us, xs, um, np.vstack((mStateScales, mParamScales)), dp) + + # Return joint gramian (partition) + if flags[10] != 0: + return V - if nf[10]: - return V # Joint gramian partition + WX = V[:nStates, :nStates] # Cross gramian + WM = V[:nStates, nStates:] # Mixed Block - WX = V[0:N, 0:N] # Cross gramian - WM = V[0:N, N:N + P] # Mixed Block + # Cross-identifiability Gramian via Schur Complement + if flags[9] == 1: # Coarse Schur-complement via identity + WI = 0.5 * (WM.T).dot(WM) + elif flags[9] == 2: # Exact Schur-complement via pseudo-inverse + WI = 0.5 * (WM.T).dot(np.linalg.pinv(WX + WX.T)).dot(WM) + else: # Approximate Schur-complement via approximate inverse + WI = 0.5 * (WM.T).dot(ainv(WX + WX.T)).dot(WM) - if not nf[9]: # Cross-identifiability gramian - WI = 0.5 * WM.T.dot(ainv(WX + WX.T)).dot(WM) - else: # Coarse Schur-complement via identity - WI = 0.5 * WM.T.dot(WM) return WX, WI assert False, "emgr: unknown gramian type!" ############################################################################### -# LOCAL FUNCTION: scales +# LOCAL FUNCTION: ident ############################################################################### -def scales(nf1, nf2): - """ Input and initial state perturbation scales """ - - if nf1 == 1: # Linear - s = np.array([0.25, 0.50, 0.75, 1.0], ndmin=1) - - elif nf1 == 2: # Geometric - s = np.array([0.125, 0.25, 0.5, 1.0], ndmin=1) - - elif nf1 == 3: # Logarithmic - s = np.array([0.001, 0.01, 0.1, 1.0], ndmin=1) - - elif nf1 == 4: # Sparse - s = np.array([0.01, 0.50, 0.99, 1.0], ndmin=1) - - else: - s = np.array([1.0], ndmin=1) - - if nf2 == 0: - s = np.concatenate((-s, s)) +def ident(x, u, p, t): + """ (Output) identity function """ - return s + return x ############################################################################### -# LOCAL FUNCTION: pscales +# LOCAL FUNCTION: scales ############################################################################### -def pscales(p, nf, ns): - """ Parameter perturbation scales """ +def scales(flScales, flRot): + """ Input and initial state perturbation scales """ - assert p.shape[1] >= 2, "emgr: min and max parameter requires!" + if flScales == 1: # Linear + mScales = np.array([0.25, 0.50, 0.75, 1.0], ndmin=1) - pmin = np.amin(p, axis=1) - pmax = np.amax(p, axis=1) + elif flScales == 2: # Geometric + mScales = np.array([0.125, 0.25, 0.5, 1.0], ndmin=1) - if nf == 1: # Linear centering and scales - pr = 0.5 * (pmax + pmin) - pm = np.outer(pmax - pmin, np.linspace(0, 1.0, ns)) + (pmin - pr)[:, np.newaxis] + elif flScales == 3: # Logarithmic + mScales = np.array([0.001, 0.01, 0.1, 1.0], ndmin=1) - elif nf == 2: # Logarithmic centering and scales - lmin = np.log(pmin) - lmax = np.log(pmax) - pr = np.real(np.exp(0.5 * (lmax + lmin))) - pm = np.real(np.exp(np.outer(lmax - lmin, np.linspace(0, 1.0, ns)) + lmin[:, np.newaxis])) - pr[:, np.newaxis] + elif flScales == 4: # Sparse + mScales = np.array([0.01, 0.50, 0.99, 1.0], ndmin=1) - else: # No centering and linear scales - pr = np.reshape(pmin, (pmin.size, 1)) - pm = np.outer(pmax - pmin, np.linspace(1.0 / ns, 1.0, ns)) + else: + mScales = np.array([1.0], ndmin=1) - return pr, pm + if flRot == 0: + mScales = np.concatenate((-mScales, mScales)) + + return mScales ############################################################################### -# LOCAL FUNCTION: ident +# LOCAL FUNCTION: pscales ############################################################################### -def ident(x, u, p, t): - """ (Output) identity function """ +def paramScales(p, flScales, nParamScales): + """ Parameter perturbation scales """ - return x + vParamMin = np.amin(p, axis=1) + vParamMax = np.amax(p, axis=1) + + if flScales == 1: # Linear centering and scales + assert p.shape[1] >= 2, "emgr: min and max parameter requires!" + vParamSteady = 0.5 * (vParamMax + vParamMin) + vScales = np.linspace(0.0, 1.0, nParamScales) + + elif flScales == 2: # Logarithmic centering and scales + assert p.shape[1] >= 2, "emgr: min and max parameter requires!" + vParamSteady = np.sqrt(vParamMax * vParamMin) + vParamMin = np.log(vParamMin) + vParamMax = np.log(vParamMax) + vScales = np.linspace(0.0, 1.0, nParamScales) + + elif flScales == 3: # Nominal centering and scaling + assert p.shape[1] >= 3, "emgr: min, nom, max parameter requires!" + vParamSteady = p[:, 1] + vParamMin = p[:, 0] + vParamMax = p[:, 2] + vScales = np.linspace(0.0, 1.0, nParamScales) + + else: # No centering and linear scales + assert p.shape[1] >= 2, "emgr: min and max parameter requires!" + vParamSteady = np.copy(vParamMin) + vParamMin = np.full(p.shape[0], 1.0 / nParamScales) + vScales = np.linspace(1.0 / nParamScales, 1.0, nParamScales) + + mParamScales = np.outer(vParamMax - vParamMin, vScales.T) + np.expand_dims(vParamMin, -1) + if flScales == 2: + mParamScales = np.exp(mParamScales) + vParamSteady = np.expand_dims(vParamSteady, -1) + mParamScales -= vParamSteady + + return vParamSteady, mParamScales ############################################################################### # LOCAL FUNCTION: ainv @@ -622,12 +672,12 @@ def ainv(m): """ Quadratic complexity approximate inverse matrix """ # Based on truncated Neumann series: X = D^-1 - D^-1 (M - D) D^-1 - d = np.copy(np.diag(m)) - k = np.nonzero(np.fabs(d) > np.sqrt(np.spacing(1))) + d = np.copy(np.diag(m))[:, np.newaxis] + k = np.nonzero(np.abs(d) > np.sqrt(np.spacing(1))) d[k] = 1.0 / d[k] - x = m * (-d) - x *= d.T + x = (m * (-d)) * d.T x.flat[::np.size(d) + 1] = d + return x ############################################################################### @@ -635,29 +685,28 @@ def ainv(m): ############################################################################### -STAGES = 3 # Configurable number of stages for increased stability of ssp2 +STAGES = 3 # Configurable number of stages for enhanced stability def ssp2(f, g, t, x0, u, p): """ Low-Storage Strong-Stability-Preserving Second-Order Runge-Kutta """ - dt = t[0] - nt = int(math.floor(t[1] / dt) + 1) + nStages = STAGES if isinstance(STAGES, int) else 3 + + tStep = t[0] + nSteps = int(math.floor(t[1] / tStep) + 1) y0 = g(x0, u(0), p, 0) - y = np.zeros((y0.shape[0], nt)) # Pre-allocate trajectory - y[:, 0] = y0 + y = np.empty((y0.shape[0], nSteps)) # Pre-allocate trajectory + y[:, 0] = y0.T xk1 = np.copy(x0) - xk2 = np.copy(x0) - for k in range(1, nt): - tk = (k - 0.5) * dt - uk = u(tk) - for _ in range(STAGES - 1): - xk1 += (dt / (STAGES - 1.0)) * f(xk1, uk, p, tk) - xk2 += dt * f(xk1, uk, p, tk) - xk2 /= STAGES - xk2 += xk1 * ((STAGES - 1.0) / STAGES) - xk1 = np.copy(xk2) - y[:, k] = g(xk1, uk, p, tk).flatten(0) + for k in range(1, nSteps): + xk2 = np.copy(xk1) + tCurr = (k - 0.5) * tStep + uCurr = u(tCurr) + for _ in range(1, nStages): + xk1 += (tStep / (STAGES - 1.0)) * f(xk1, uCurr, p, tCurr) + xk1 = (xk1 * (nStages - 1) + xk2 + tStep * f(xk1, uCurr, p, tCurr)) / nStages + y[:, k] = g(xk1, uCurr, p, tCurr).flatten(0) return y diff --git a/py/emgrProbe.py b/py/emgrProbe.py index 402c434..9431a60 100755 --- a/py/emgrProbe.py +++ b/py/emgrProbe.py @@ -2,7 +2,7 @@ """ project: emgr ( https://gramian.de ) - version: 5.9.py (2021-01-21) + version: 5.99.py (2021-04-13) authors: Christian Himpe (0000-0003-2194-6754) license: BSD-2-Clause License (opensource.org/licenses/BSD-2-Clause) summary: Factorial empirical Gramian singular value decay testing @@ -41,22 +41,22 @@ def G(x, u, p, t): # output functional Tf = 1.0 p = np.zeros((N, 1)) - q = np.ones((N, 1)).dot([[0.5, 1.0]]) + q = np.ones((N, 1)).dot([[0.5, 0.75, 1.0]]) gramian = ["c", "o", "x", "y", "s", "i", "j"] # controllability, observability, minimality, linear minimality, sensitivity, identifiability, cross-identifiability kernels = [np.dot] #, quadratic, cubic, sigmoid] training = ["i", "s", "h", "a", "r"] # impulse, step, havercosine-chirp, sinc, pseudo-random-binary - weighting = [0] #, 1, 2, 3, 4] # none, linear-time, quadratic-time, per-state, per-component - centering = [0] #, 1, 2, 3, 4, 5] # none, steady-state, final-state, arithmetic-mean, root-mean-square, midrange + weighting = [0, 1, 2, 3, 4, 5] # none, linear-time, quadratic-time, per-state, per-component, reciprocal + centering = [0, 1, 2, 3, 4, 5] # none, steady-state, final-state, arithmetic-mean, root-mean-square, midrange normalization = [0, 1, 2] # none, steady-state, Jacobi-type stype = [0, 1] # standard, (output-controllability, average-observability, non-symmetric-cross-gramian) extra = [0, 1] # none, extra-input - scales = [0]#, 1, 2, 3, 4] # single, linear, geometric, logarithmic, sparse - rotations = [0]#, 1] # positive-negative, single + scales = [0, 1, 2, 3, 4] # single, linear, geometric, logarithmic, sparse + rotations = [0, 1] # positive-negative, single - pcentering = [0]#, 1, 2] # none, linear, logarithmic + pcentering = [0, 1, 2, 3] # none, linear, logarithmic, nominal ptype = [0, 1] # standard (input-output-sensitivity, coarse-schur-complement) z = 0 @@ -79,7 +79,12 @@ def G(x, u, p, t): # output functional for j in ptype: for k in pcentering: - W.append(np.linalg.svd(emgr(F, K, [M, N, Q], [dt, Tf], w, q, [d, 0, 0, 0, 0, e, f, g, 0, j, k, 0, h], c, 0.0, 0.0, 1.0, 1.0, i)[1], compute_uv=False)) + X,P = emgr(F, K, [M, N, Q], [dt, Tf], w, q, [d, 0, 0, 0, 0, e, f, g, j, k, 0, 0, h], c, 0.0, 0.0, 1.0, 1.0, i) + if w == "s": + W.append(P) + else: + W.append(np.linalg.svd(P, compute_uv=False)) + else: W.append(np.linalg.svd(emgr(F, K, [M, N, Q], [dt, Tf], w, p, [d, 0, 0, 0, 0, e, f, g, 0, 0, 0, 0, h], c, 0.0, 0.0, 1.0, 1.0, i), compute_uv=False)) diff --git a/test_dwj.m b/test_dwj.m new file mode 100644 index 0000000..9588f73 --- /dev/null +++ b/test_dwj.m @@ -0,0 +1,101 @@ +function test_dwj(o) +%%% project: emgr - EMpirical GRamian Framework ( https://gramian.de ) +%%% version: 5.99 (2022-04-13) +%%% authors: Christian Himpe (0000-0003-2194-6754) +%%% license: BSD-2-Clause (opensource.org/licenses/BSD-2-Clause) +%%% summary: test_dwj (distributed cross-identifiability gramian reduction) + + if(exist('emgr')~=2) + error('emgr not found! Get emgr at: http://gramian.de'); + else + global ODE; + ODE = []; + fprintf('emgr (version: %1.2f)\n',emgr('version')); + end%if + +%% SYSTEM SETUP + M = 4; % number of inputs + N = 10; % number of states + Q = M; % number of outputs + h = 0.01; % time step size + T = 1.0; % time horizon + X = zeros(N,1); % initial state + U = @(t) ones(M,1)*(t<=h)/h; % impulse input function + P = 0.5+0.5*cos(1:N)'; % parameter + R = [0.5*ones(N,1),ones(N,1)]; % parameter range + + A = -gallery('lehmer',N); % system matrix + B = toeplitz(1:N,1:M)./N; % input matrix + C = B'; % output matrix + + LIN = @(x,u,p,t) A*x + B*u + p; % vector field + OUT = @(x,u,p,t) C*x; % output functional + +%% FULL ORDER MODEL REFERENCE SOLUTION + Y = ODE(LIN,OUT,[h,T],X,U,P); + %figure; plot(0:h:T,Y); return; + n1 = norm(Y(:),1); + n2 = norm(Y(:),2); + n8 = norm(Y(:),Inf); + +%% COMPARATIVE REDUCED ORDER MODEL PROJECTION ASSEMBLY + tic; + WJ = emgr(LIN,OUT,[M,N,Q],[h,T],'j',R,[0,0,0,0,0,0,0,0,0,0,0,0,0]); + [UU,D,VV] = svd(WJ{2}); + OFFLINE_TIME_FULL = toc + + tic; + + for w = 1:10 + w + K = ceil(N/w) + ceil(N/w) + wj = cell(1,K); + for k=1:K + wj{k} = emgr(LIN,OUT,[M,N,Q],[h,T],'j',R,[0,0,0,0,0,0,0,0,0,0,w,k,0]); + end%for + wj = cell2mat(wj); + wx = wj(:,1:N); + wm = wj(:,N+1:end); + wii = 0.5 * (wm' * ainv(wx+wx') * wm); + + OFFLINE_TIME_DIST = toc + RESIDUAL_1 = norm(WJ{1}-wx) + RESIDUAL_2 = norm(WJ{2}-wii) + end%for + +%% REDUCED ORDER MODEL EVALUATION + l1 = zeros(1,N-1); + l2 = zeros(1,N-1); + l8 = zeros(1,N-1); + + for n=1:N-1 + uu = UU(:,1:n); + y = ODE(LIN,OUT,[h,T],X,U,uu*uu'*P); + l1(n) = norm(Y(:)-y(:),1)/n1; + l2(n) = norm(Y(:)-y(:),2)/n2; + l8(n) = norm(Y(:)-y(:),Inf)/n8; + end%for + +%% PLOT REDUCED ORDER VS RELATIVE ERRORS + figure('Name',mfilename,'NumberTitle','off'); + semilogy(1:N-1,l1,'r','linewidth',2); hold on; + semilogy(1:N-1,l2,'g','linewidth',2); + semilogy(1:N-1,l8,'b','linewidth',2); hold off; + xlim([1,N-1]); + ylim([1e-16,1]); + pbaspect([2,1,1]); + legend('L1 Error ','L2 Error ','L8 Error ','location','northeast'); + set(gca,'YGrid','on'); +end + +function x = ainv(m) +%%% summary: ainv (approximate inverse) + + D = diag(m); + k = find(abs(D) > sqrt(eps)); + D(k) = 1.0 ./ D(k); + x = m .* (-D); + x = x .* (D'); + x(1:numel(D) + 1:end) = D; +end +