diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..39f7ecf806 --- /dev/null +++ b/404.html @@ -0,0 +1,3151 @@ + + + + + + + + + + + + + + + + + + + + + + + PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 0000000000..85449ec798 --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,119 @@ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +/* Backward-compatibility: docstring section titles in bold. */ +.doc-section-title { + font-weight: bold; +} + +/* Symbols in Navigation and ToC. */ +:root, +[data-md-color-scheme="default"] { + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} + +.doc-signature .autorefs { + color: inherit; + border-bottom: 1px dotted currentcolor; +} diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000..1cf13b9f9d Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.ad660dcc.min.js b/assets/javascripts/bundle.ad660dcc.min.js new file mode 100644 index 0000000000..0ffc0460a4 --- /dev/null +++ b/assets/javascripts/bundle.ad660dcc.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var O=f()(_);return u("cut"),O},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",O=document.createElement("textarea");O.style.fontSize="12pt",O.style.border="0",O.style.padding="0",O.style.margin="0",O.style.position="absolute",O.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return O.style.top="".concat(j,"px"),O.setAttribute("readonly",""),O.value=V,O}var te=function(_,O){var j=A(_);O.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var O=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,O):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,O):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(O){return typeof O}:H=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=_.action,j=O===void 0?"copy":O,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(O){return typeof O}:Ie=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var O=0;O<_.length;O++){var j=_[O];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,O){return _&&ro(V.prototype,_),O&&ro(V,O),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(O){return O.__proto__||Object.getPrototypeOf(O)},Wt(V)}function vr(V,_){var O="data-clipboard-".concat(V);if(_.hasAttribute(O))return _.getAttribute(O)}var Ri=function(V){Ci(O,V);var _=Hi(O);function O(j,D){var Y;return _i(this,O),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(O,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),O}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var M=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return M}:y(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return y(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),B(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),Te(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(E(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function tt(e){return Ea.pipe(E(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?M:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(G("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),B(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():M))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),B(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),B(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),B(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),B(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function ye(){return St}function G(e){return St.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!G("announce.dismiss")||!e.childElementCount)return M;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(E(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function wn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,c)," "],[]).slice(0,-1),i=ye(),a=new URL(e.location,i.base);G("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=ye();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=ye(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function Sn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ca(e){var o;let t=ye(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Mn(e,t){var o;let r=ye();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:M),E(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(E(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?M:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):M})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||G("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),G("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||G("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:M)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(E(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return G("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),E(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(E(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),B(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=x("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e),tt(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),E(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return G("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),Na(n).pipe(E(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>G("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(E(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return M;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!G("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),B(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>G("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?M:Ya(o,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),B(1))}function Kn(e){let t=$("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(E(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(E(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return M;let r=e.target.closest("a");if(r===null)return M;if(r.target||e.metaKey||e.ctrlKey)return M;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):M}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...G("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),M}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=ye();if(location.protocol==="file:")return M;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),M)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),s.pipe(v(()=>e),Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),E(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:G("search.suggest")}}})),r}function ii({document$:e}){let t=ye(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>M)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?M:(i.preventDefault(),I(p))}}return M}),v(i=>ur(new URL(i)).pipe(m(a=>{let p=xe().href.replace(t.base,i);return a.has(p.split("#")[0])?new URL(p):new URL(i)})))))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),B(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(E(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),B(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(E(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?M:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(E(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(E(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=ye();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(E(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>M),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>M),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>M),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return M}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return M}return di(e.href).pipe(E(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>M),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),B(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(G("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),G("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return G("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title")))})).subscribe(),e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),E(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),E(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),B(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=ye(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;G("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),B(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>G("search.highlight")?mi(e,{index$:Mi,location$:jt}):M),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),B(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.ad660dcc.min.js.map + diff --git a/assets/javascripts/bundle.ad660dcc.min.js.map b/assets/javascripts/bundle.ad660dcc.min.js.map new file mode 100644 index 0000000000..6d61170f1e --- /dev/null +++ b/assets/javascripts/bundle.ad660dcc.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an
+

速度 u 随时间的动态变化图像
+
+
速度 v 随时间的动态变化图像
+
+
压力 p 随时间的动态变化图像
+
+
水平集函数(气泡的形状) phi 随时间的动态变化图像
+

从动态变化图像可以得出以下结论:

+
    +
  • 从水平集函数(气泡的形状) \(\phi\) 随时间的动态变化图像,可以看出该模型可以很好的预测气泡在液体管中的变化过程,具有良好的精度;
  • +
  • 从速度 \(u,~v\) 随时间的动态变化图像,可以看出该模型可以很好的预测气泡在液体管中变化时的速度大小,同时对速度的预测优于传统 DNN 方法,具体比较可以参考文章
  • +
  • 然而观察压力 \(p\) 随时间的动态变化图像,几乎不怎么变化, 并没有成功捕获压力场中的气泡形状特征细节,这是因为,与大压力范围(动态变化图像中压力 \(p\) 的范围[800, 810] )相比,描绘气泡形状的压力大小的细微差别太小。
  • +
+

综上所述,物理信息与传统神经网络相结合的 Semi-PINNs 方法可以灵活地构建网络框架,获得满足工程需求的满意结果,尤其是在不易获取大量训练数据时是非常有效的。虽然编码完整的流体动力学方程的深度神经网络在预测上可能更准确,但目前的 BubbleNet 本质上是面向工程的 Semi-PINNs 方法,具有简单、计算效率和灵活性的优势。这就提出了一个值得未来继续研究的有趣问题,即我们可以通过选择性地将物理信息引入神经网络来优化网络性能,更多相关内容及结论请参考文章

+

6. 参考资料

+

参考文献: Predicting micro-bubble dynamics with semi-physics-informed deep learning

+

参考代码: BubbleNet(Semi-PINNs)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/cfdgcn/index.html b/zh/examples/cfdgcn/index.html new file mode 100644 index 0000000000..555a98eb4b --- /dev/null +++ b/zh/examples/cfdgcn/index.html @@ -0,0 +1,4474 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CFDGCN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Combining Differentiable PDE Solvers and Graph Neural Networks for Fluid Flow Prediction

+

AI Studio快速体验

+
+
+
+
# only linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/data.zip
+unzip data.zip
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/meshes.tar
+tar -xvf meshes.tar
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/SU2Bin.tgz
+tar -zxvf SU2Bin.tgz
+
+# set BATCH_SIZE = number of cpu cores
+export BATCH_SIZE=4
+
+# prediction experiments
+mpirun -np $((BATCH_SIZE+1)) python cfdgcn.py \
+  TRAIN.batch_size=$((BATCH_SIZE)) > /dev/null
+
+# generalization experiments
+mpirun -np $((BATCH_SIZE+1)) python cfdgcn.py \
+  TRAIN.batch_size=$((BATCH_SIZE)) \
+  TRAIN_DATA_DIR="./data/NACA0012_machsplit_noshock/outputs_train" \  
+  TRAIN_MESH_GRAPH_PATH="./data/NACA0012_machsplit_noshock/mesh_fine. su2" \
+  EVAL_DATA_DIR="./data/NACA0012_machsplit_noshock/outputs_test" \
+  EVAL_MESH_GRAPH_PATH="./data/NACA0012_machsplit_noshock/mesh_fine.su2" \
+  > /dev/null
+
+
+
+
+

1. 背景简介

+

近年来,深度学习在计算机视觉和自然语言处理方面的成功应用,促使人们探索人工智能在科学计算领域的应用,尤其是在计算流体力学(CFD)领域的应用。

+

流体是非常复杂的物理系统,流体的行为由 Navier-Stokes 方程控制。基于网格的有限体积或有限元模拟方法是 CFD 中广泛使用的数值方法。计算流体动力学研究的物理问题往往非常复杂,通常需要大量的计算资源才能求出问题的解,因此需要在求解精度和计算成本之间进行权衡。为了进行数值模拟,计算域通常被网格离散化,由于网格具有良好的几何和物理问题表示能力,同时和图结构相契合,所以这篇文章的作者使用图神经网络,通过训练 CFD 仿真数据,构建了一种数据驱动模型来进行流场预测。

+

2. 问题定义

+

作者提出了一种基于图神经网络的 CFD 计算模型,称为 CFD-GCN (Computational fluid dynamics - Graph convolution network),该模型是一种混合的图神经网络,它将传统的图卷积网络与粗分辨率的 CFD 模拟器相结合,不仅可以大幅度加速 CFD 预测,还可以很好地泛化到新的场景,与此同时,模型的预测效果远远优于单独的粗分辨率 CFD 的模拟效果。

+

下图为该方法的网络结构图,网络有两个主要部件:GCN 图神经网络以及 SU2 流体模拟器。网络在两个不同的图上运行,两个图分别是细网格的图和粗网格的图。网络首先在粗网格上运行 CFD 模拟,同时使用 GCN 处理细网格的图。然后,对模拟结果进行上采样,并将结果与 GCN 的中间输出连接起来。最后,模型将额外的 GCN 层应用于这些连接特征,预测所需的输出值。

+

CFDGCN_overview

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+
+

注意事项

+

本案例运行前需通过 pip install pgl==2.2.6 mpi4py 命令,安装 Paddle Graph Learning 图学习工具和 Mpi4py MPI python接口库。

+

由于新版本的 Paddle 依赖的 python 版本较高,pglmpi4py 的安装可能会出现问题,建议使用AI Studio快速体验,项目中已经配置好运行环境。

+
+

3.1 数据集下载

+

该案例使用的机翼数据集 Airfoil来自 de Avila Belbute-Peres 等人,其中翼型数据集采用 NACA0012 翼型,包括 train, test 以及对应的网格数据 mesh_fine;圆柱数据集是原作者利用软件计算的 CFD 算例。

+

执行以下命令,下载并解压数据集。

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/data.zip
+unzip data.zip
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/meshes.tar
+tar -xvf meshes.tar
+
+

3.2 SU2 预编译库安装

+

SU2 流体模拟器以预编译库的形式嵌入在网络中,我们需要下载并设置环境变量。

+

执行以下命令,下载并解压预编译库。

+
wget -nc -P https://paddle-org.bj.bcebos.com/paddlescience/datasets/CFDGCN/SU2Bin.tgz
+tar -zxvf SU2Bin.tgz
+
+

预编译库下载完成后,设置 SU2 的环境变量。

+
export SU2_RUN=/absolute_path/to/SU2Bin/
+export SU2_HOME=/absolute_path/to/SU2Bin/
+export PATH=$PATH:$SU2_RUN
+export PYTHONPATH=$PYTHONPATH:$SU2_RUN
+
+

3.3 模型构建

+

在本问题中,我们使用神经网络 CFDGCN 作为模型,其接收图结构数据,输出预测结果。

+
77
+78
+79
+80
+81
+82
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    output_expr={"pred": lambda out: out["pred"]},
+    loss=ppsci.loss.FunctionalLoss(train_mse_func),
+    name="Sup",
+)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("input", ),输出变量名是 ("pred", ),这些命名与后续代码保持一致。

+

3.4 约束构建

+

在本案例中,我们使用监督数据集对模型进行训练,因此需要构建监督约束。

+

在定义约束之前,我们需要指定数据集的路径等相关配置,将这些信息存放到对应的 YAML 文件中,如下所示。

+
28
+29
+30
+31
+32
+33
+34
# set training data path
+TRAIN_DATA_DIR: "./data/NACA0012_interpolate/outputs_train"
+TRAIN_MESH_GRAPH_PATH: "./data/NACA0012_interpolate/mesh_fine.su2"
+
+# set evaluate data path
+EVAL_DATA_DIR: "./data/NACA0012_interpolate/outputs_test"
+EVAL_MESH_GRAPH_PATH: "./data/NACA0012_interpolate/mesh_fine.su2"
+
+

接着定义训练损失函数的计算过程,如下所示。

+
31
+32
+33
+34
+35
+36
def train_mse_func(
+    output_dict: Dict[str, "paddle.Tensor"],
+    label_dict: Dict[str, "pgl.Graph"],
+    *args,
+) -> paddle.Tensor:
+    return {"pred": F.mse_loss(output_dict["pred"], label_dict["label"].y)}
+
+

最后构建监督约束,如下所示。

+
58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
train_dataloader_cfg = {
+    "dataset": {
+        "name": "MeshAirfoilDataset",
+        "input_keys": ("input",),
+        "label_keys": ("label",),
+        "data_dir": cfg.TRAIN_DATA_DIR,
+        "mesh_graph_path": cfg.TRAIN_MESH_GRAPH_PATH,
+        "transpose_edges": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": True,
+    },
+    "num_workers": 1,
+}
+
+# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    output_expr={"pred": lambda out: out["pred"]},
+    loss=ppsci.loss.FunctionalLoss(train_mse_func),
+    name="Sup",
+)
+# wrap constraints together
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.5 超参数设定

+

设置训练轮数等参数,如下所示。

+
50
+51
+52
+53
+54
+55
+56
epochs: 500
+iters_per_epoch: 42
+save_freq: 50
+eval_during_train: true
+eval_freq: 50
+learning_rate: 5.0e-4
+batch_size: 4
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并使用固定的 5e-4 作为学习率。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器,构建过程与 约束构建 类似,只需把数据目录改为测试集的目录,并在配置文件中设置 EVAL.batch_size=1 即可。

+
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "MeshAirfoilDataset",
+        "input_keys": ("input",),
+        "label_keys": ("label",),
+        "data_dir": cfg.EVAL_DATA_DIR,
+        "mesh_graph_path": cfg.EVAL_MESH_GRAPH_PATH,
+        "transpose_edges": True,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+}
+rmse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(train_mse_func),
+    output_expr={"pred": lambda out: out["pred"].unsqueeze(0)},
+    metric={"RMSE": ppsci.metric.FunctionalMetric(eval_rmse_func)},
+    name="RMSE_validator",
+)
+validator = {rmse_validator.name: rmse_validator}
+
+

评估指标为预测结果和真实结果的 RMSE 值,因此需自定义指标计算函数,如下所示。

+
39
+40
+41
+42
+43
+44
+45
+46
+47
+48
def eval_rmse_func(
+    output_dict: Dict[str, List["paddle.Tensor"]],
+    label_dict: Dict[str, List["pgl.Graph"]],
+    *args,
+) -> Dict[str, paddle.Tensor]:
+    mse_losses = [
+        F.mse_loss(pred, label.y)
+        for (pred, label) in zip(output_dict["pred"], label_dict["label"])
+    ]
+    return {"RMSE": (sum(mse_losses) / len(mse_losses)) ** 0.5}
+
+

评估指标为预测结果和真实结果的 RMSE 值,因此需自定义指标计算函数,如下所示。

+

3.8 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    validator=validator,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+
+

3.9 结果可视化

+

训练完毕之后程序会对测试集中的数据进行预测,并以图片的形式对结果进行可视化,如下所示。

+
# visualize prediction
+with solver.no_grad_context_manager(True):
+    for index, (input_, label, _) in enumerate(rmse_validator.data_loader):
+        truefield = label["label"].y
+        prefield = model(input_)
+        utils.log_images(
+            input_["input"].pos,
+            prefield["pred"],
+            truefield,
+            rmse_validator.data_loader.dataset.elems_list,
+            index,
+            "cylinder",
+        )
+
+

4. 完整代码

+
cfdgcn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from typing import Dict
+from typing import List
+
+import hydra
+import paddle
+import pgl
+import su2paddle
+import utils
+from omegaconf import DictConfig
+from paddle.nn import functional as F
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train_mse_func(
+    output_dict: Dict[str, "paddle.Tensor"],
+    label_dict: Dict[str, "pgl.Graph"],
+    *args,
+) -> paddle.Tensor:
+    return {"pred": F.mse_loss(output_dict["pred"], label_dict["label"].y)}
+
+
+def eval_rmse_func(
+    output_dict: Dict[str, List["paddle.Tensor"]],
+    label_dict: Dict[str, List["pgl.Graph"]],
+    *args,
+) -> Dict[str, paddle.Tensor]:
+    mse_losses = [
+        F.mse_loss(pred, label.y)
+        for (pred, label) in zip(output_dict["pred"], label_dict["label"])
+    ]
+    return {"RMSE": (sum(mse_losses) / len(mse_losses)) ** 0.5}
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", os.path.join(cfg.output_dir, "train.log"), "info")
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "MeshAirfoilDataset",
+            "input_keys": ("input",),
+            "label_keys": ("label",),
+            "data_dir": cfg.TRAIN_DATA_DIR,
+            "mesh_graph_path": cfg.TRAIN_MESH_GRAPH_PATH,
+            "transpose_edges": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+        "num_workers": 1,
+    }
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        output_expr={"pred": lambda out: out["pred"]},
+        loss=ppsci.loss.FunctionalLoss(train_mse_func),
+        name="Sup",
+    )
+    # wrap constraints together
+    constraint = {sup_constraint.name: sup_constraint}
+    process_sim = sup_constraint.data_loader.dataset._preprocess
+    fine_marker_dict = sup_constraint.data_loader.dataset.marker_dict
+
+    # set model
+    model = ppsci.arch.CFDGCN(
+        **cfg.MODEL,
+        process_sim=process_sim,
+        fine_marker_dict=fine_marker_dict,
+        su2_module=su2paddle.SU2Module,
+    )
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "MeshAirfoilDataset",
+            "input_keys": ("input",),
+            "label_keys": ("label",),
+            "data_dir": cfg.EVAL_DATA_DIR,
+            "mesh_graph_path": cfg.EVAL_MESH_GRAPH_PATH,
+            "transpose_edges": True,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+    rmse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(train_mse_func),
+        output_expr={"pred": lambda out: out["pred"].unsqueeze(0)},
+        metric={"RMSE": ppsci.metric.FunctionalMetric(eval_rmse_func)},
+        name="RMSE_validator",
+    )
+    validator = {rmse_validator.name: rmse_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+
+    # train model
+    solver.train()
+
+    # visualize prediction
+    with solver.no_grad_context_manager(True):
+        for index, (input_, label, _) in enumerate(rmse_validator.data_loader):
+            truefield = label["label"].y
+            prefield = model(input_)
+            utils.log_images(
+                input_["input"].pos,
+                prefield["pred"],
+                truefield,
+                rmse_validator.data_loader.dataset.elems_list,
+                index,
+                "cylinder",
+            )
+
+
+def evaluate(cfg: DictConfig):
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "MeshAirfoilDataset",
+            "input_keys": ("input",),
+            "label_keys": ("label",),
+            "data_dir": cfg.TRAIN_DATA_DIR,
+            "mesh_graph_path": cfg.TRAIN_MESH_GRAPH_PATH,
+            "transpose_edges": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+        "num_workers": 1,
+    }
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        output_expr={"pred": lambda out: out["pred"]},
+        loss=ppsci.loss.FunctionalLoss(train_mse_func),
+        name="Sup",
+    )
+
+    process_sim = sup_constraint.data_loader.dataset._preprocess
+    fine_marker_dict = sup_constraint.data_loader.dataset.marker_dict
+
+    # set airfoil model
+    model = ppsci.arch.CFDGCN(
+        **cfg.MODEL,
+        process_sim=process_sim,
+        fine_marker_dict=fine_marker_dict,
+        su2_module=su2paddle.SU2Module,
+    )
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "MeshAirfoilDataset",
+            "input_keys": ("input",),
+            "label_keys": ("label",),
+            "data_dir": cfg.EVAL_DATA_DIR,
+            "mesh_graph_path": cfg.EVAL_MESH_GRAPH_PATH,
+            "transpose_edges": True,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+    rmse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(train_mse_func),
+        output_expr={"pred": lambda out: out["pred"].unsqueeze(0)},
+        metric={"RMSE": ppsci.metric.FunctionalMetric(eval_rmse_func)},
+        name="RMSE_validator",
+    )
+    validator = {rmse_validator.name: rmse_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # evaluate model
+    solver.eval()
+
+    # visualize prediction
+    with solver.no_grad_context_manager(True):
+        for index, (input_, label, _) in enumerate(rmse_validator.data_loader):
+            truefield = label["label"].y
+            prefield = model(input_)
+            utils.log_images(
+                input_["input"].pos,
+                prefield["pred"],
+                truefield,
+                rmse_validator.data_loader.dataset.elems_list,
+                index,
+                "cylinder",
+            )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="cfdgcn.yaml")
+def main(cfg: DictConfig):
+    su2paddle.activate_su2_mpi(remove_temp_files=True)
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下方展示了模型对计算域中每个点的压力\(p(x,y)\)、x(水平)方向流速\(u(x,y)\)、y(垂直)方向流速\(v(x,y)\)的预测结果与参考结果。

+
+
+
+

+ Airfoil_0_vec_x +
左:预测 x 方向流速 p,右:实际 x 方向流速
+ Airfoil_0_p +
左:预测压力 p,右:实际压力 p
+ Airfoil_0_vec_y +
左:预测y方向流速 p,右:实际 y 方向流速
+

+
+
+

+ Airfoil_0_vec_x +
左:预测 x 方向流速 p,右:实际 x 方向流速
+ Airfoil_0_p +
左:预测压力 p,右:实际压力 p
+ Airfoil_0_vec_y +
左:预测y方向流速 p,右:实际 y 方向流速
+

+
+
+
+

可以看到模型预测结果与真实结果基本一致,模型泛化效果良好。

+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/chip_heat/index.html b/zh/examples/chip_heat/index.html new file mode 100644 index 0000000000..ab4865ffc1 --- /dev/null +++ b/zh/examples/chip_heat/index.html @@ -0,0 +1,6186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Chip_heat - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Chip Heat Simulation

+

AI Studio快速体验

+
+
+
+
python chip_heat.py
+
+
+
+
python chip_heat.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/ChipHeat/chip_heat_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
chip_heat_pretrained.pdparamsMSE.chip(down_mse): 0.04177
MSE.chip(left_mse): 0.01783
MSE.chip(right_mse): 0.03767
MSE.chip(top_mse): 0.05034
+

1. 背景简介

+

芯片热仿真研究主要聚焦于预测和分析集成电路(IC)在操作过程中的温度分布,以及热效应对芯片性能、功耗、可靠性和寿命的影响。随着电子设备向更高性能、更高密度和更小尺寸发展,热管理成为芯片设计和制造中的一个关键挑战。

+

芯片热仿真研究为理解和解决芯片热管理问题提供了重要工具和方法,对于提高芯片的性能、降低功耗、保证可靠性和延长寿命有着至关重要的作用。随着电子设备朝着更高性能和更紧凑的方向发展,热仿真研究的重要性将会进一步增加。

+

芯片热仿真在工程和科学领域具有多方面的重要性,主要体现在以下几个方面:

+
    +
  • 设计优化和验证: 芯片热仿真可以帮助工程师和科学家在设计初期评估不同结构和材料的热特性,以优化设计并验证其可靠性。通过仿真模拟不同工作负载下的温度分布和热传导效应,可以提前发现潜在的热问题并进行针对性的改进,从而降低后期开发成本和风险。
  • +
  • 热管理和散热设计: 芯片热仿真可以帮助设计有效的热管理系统和散热方案,以确保芯片在长时间高负载运行时保持在安全的工作温度范围内。通过分析芯片周围的散热结构、风扇配置、散热片设计等因素,可以优化热传导和散热效率,提高系统的稳定性和可靠性。
  • +
  • 性能预测和优化: 温度对芯片的性能和稳定性有重要影响。芯片热仿真可以帮助预测芯片在不同工作负载和环境条件下的性能表现,包括处理器速度、功耗和电子器件的寿命等方面。通过对热效应的建模和分析,可以优化芯片的设计和工作条件,以实现更好的性能和可靠性。
  • +
  • 节能和环保: 有效的热管理和散热设计可以降低系统能耗,提高能源利用效率,从而实现节能和环保的目标。通过减少系统中热量的损失和浪费,可以降低能源消耗和碳排放,减少对环境的负面影响。
  • +
+

综上所述,芯片热仿真在工程和科学领域中具有重要的作用和价值,可以帮助优化设计、提高性能、降低成本、保护环境等方面取得积极的效果。

+

2. 问题定义

+

2.1 问题描述

+

为了搭建通用的热仿真模型,我们首先对一般情况下热仿真问题进行简要描述,热仿真旨在通过全局求解热传导方程来预测给定物体的温度场,通常可以通过以下控制方程来进行表示:

+
\[ +k \Delta T(x,t) + S(x,t) = \rho c_p \dfrac{\partial T(x,t)}{\partial t},\quad \text { in } \Omega\times (0,t_{*}), +\]
+

其中 \(\Omega\subset \mathbb{R}^{n},~n=1,2,3\) 为给定物体材料的模拟区域,如图所示为一个具有随机热源分布的2D芯片模拟区域。\(T(x,t),~S(x,t)\) 分别表示在任意时空位置 \((x,t)\) 处温度和热源分布,\(t_*\) 为温度阈值。这里 \(k\)\(\rho\)\(c_p\) 均为给定物体的材料特性,分别表示材料传热系数、质量密度和比热容。为了方便,我们关注给定物体材料的静态温度场,并通过设置 \(\frac{dT}{dt}=0\) 来简化方程:

+
\[ +\tag{1} k \Delta T(x) + S(x) = 0,\quad \text { in } \Omega. +\]
+
+

domain_chip.pdf +

+
内部具有随机热源分布的 2D 芯片模拟区域,边界上可以为任意的边界条件。
+
+

对于给定物体材料的通用热仿真模型,除了要满足控制方程(1),其温度场还取决于一些关键的 PDE 配置,包括但不限于材料特性和几何参数等。

+

第一类 PDE 配置是给定物体材料的边界条件:

+
    +
  • Dirichlet边界条件: 表面上的温度场固定为 \(q_d\)
  • +
+
\[ +T = q_d. +\]
+
    +
  • Neumann边界条件: 表面上的温度通量是固定为 \(q_n\),当 \(q_n =0\) 时,表明表面完全绝缘,称为绝热边界条件。
  • +
+
\[ +\tag{2} -k \dfrac{\partial T}{\partial n} = q_n. +\]
+
    +
  • 对流边界条件:也称为牛顿边界条件,该边界条件对应于表面相同方向上的热传导和对流之间的平衡,其中 \(h\)\(T_{amb}\) 代表表面的对流系数和环境温度。
  • +
+
\[ +-k \dfrac{\partial T}{\partial n} = h(T-T_{amb}). +\]
+
    +
  • 辐射边界条件:该边界条件对应于表面上由温差产生的电磁波辐射,其中 \(\epsilon\)\(\sigma\) 分别代表热辐射系数和Stefan-Boltzmann系数。
  • +
+
\[ +-k \dfrac{\partial T}{\partial n} = \epsilon \sigma (T^4-T_{amb}^4). +\]
+

第二类PDE配置是给定物体材料的边界或内部热源的位置和强度。本工作考虑了以下两种类型的热源:

+
    +
  • 边界随机热源:由 Neumann 边界条件(2)定义,此时 \(q_n\) 为关于 \(x\) 的函数,即任意给定的温度通量分布;
  • +
  • 内部随机热源:由控制方程(1)定义,此时 \(S(x)\) 为关于 \(x\) 的函数,即任意给定的热源分布。
  • +
+

我们的目的是,在给定的物体材料的通用热仿真模型上,输入任意的第一类或第二类设计配置,我们均可以得到对应的温度场分布情况,在边界上我们任意指定边界类型和参数。值得注意的是,这项工作中开发的通用热仿真的 PI-DeepONet 方法并不限于 第一类或第二类设计配置 条件和规则的几何形状。通过超出当前工作范围的进一步代码修改,它们可以应用于各种载荷、材料属性,甚至各种不规则的几何形状。

+

2.2 PI-DeepONet模型

+

PI-DeepONet模型,将 DeepONet 和 PINN 方法相结合,是一种结合了物理信息和算子学习的深度神经网络模型。这种模型可以通过控制方程的物理信息来增强 DeepONet 模型,同时可以将不同的 PDE 配置分别作为不同的分支网络的输入数据,从而可以有效地用于在各种(参数和非参数)PDE 配置下进行超快速的模型预测。

+

对于芯片热仿真问题,PI-DeepONet 模型可以表示为如图所示的模型结构:

+
+

pi_deeponet.pdf

+
+

如图所示,我们一共使用了 3 个分支网络和一个主干网络,分支网络分别输入边界类型指标、随机热源分布 \(S(x, y)\) 和边界函数 \(Q(x, y)\),主干网络输入二维坐标点坐标信息。每个分支网和主干网均输出 \(q\) 维特征向量,通过 Hadamard(逐元素)乘积组合所有这些输出特征,然后将所得向量相加为预测温度场的标量输出。

+

3. 问题求解

+

接下来开始讲解如何将该问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该换热器热仿真问题。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建等关键步骤进行阐述,而其余细节请参考API文档

+

3.1 模型构建

+

在芯片热仿真问题中,每一个已知的坐标点 \((x, y)\) 和每一组边界类型 \(bt\)、随机热源分布 \(S(x, y)\) 以及边界函数 \(Q(x, y)\) 都对应一组芯片的温度分布 \(T\),一个待求解的未知量。我们在这里使用 3 个分支网络和一个主干网络,4 个网络均为 MLP(Multilayer Perceptron, 多层感知机) 。 3 个分支网络分别表示 \((bt, S, Q)\) 到输出函数 \((b_1, b_2, b_3)\) 的映射函数 \(f_1,f_2,f_3: \mathbb{R}^3 \to \mathbb{R}^{q}\),即:

+
\[ +\begin{aligned} +b_1 &= f_1(bt),\\ +b_2 &= f_2(S),\\ +b_3 &= f_3(Q). +\end{aligned} +\]
+

上式中 \(f_1, f_2, f_3\) 均为 MLP 模型,\((b_1,b_2,b_3)\) 分别为三个分支网络的输出函数,\(q\) 为输出函数的维数。主干网络表示 \((x, y)\) 到输出函数 \(t_0\) 的映射函数 \(f_4: \mathbb{R} \to \mathbb{R}^{q}\),即:

+
\[ +\begin{aligned} +t_0 &= f_4(x, y). +\end{aligned} +\]
+

上式中 \(f_4\) 为 MLP 模型,\((t_0)\) 为主支网络的输出函数,\(q\) 为输出函数的维数。我们可以将三个分支网络和主干网络的输出函数 \((b_1, b_2, b_3, t_0)\) 进行 Hadamard(逐元素)乘积再相加得到标量温度场,即:

+
\[ +T = \sum_{i=1}^q b_1^ib_2^ib_3^it_0^i. +\]
+

我们定义 PaddleScience 内置的 ChipHeats 模型类,并调用,PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.ChipDeepONets(**cfg.MODEL)
+
+

这样我们就实例化出了一个拥有 4 个 MLP 模型的 ChipHeats 模型,每个分支网络包含 9 层隐藏神经元,每层神经元数为 256,主干网络包含 6 层隐藏神经元,每层神经元数为 128,使用 "Swish" 作为激活函数,并包含一个输出函数 \(T\) 的神经网络模型 model。更多相关内容请参考文献 A fast general thermal simulation model based on MultiBranch Physics-Informed deep operator neural network

+

3.2 计算域构建

+

对本文中芯片热仿真问题构造训练区域,即以 \([0, 1]\times[0, 1]\) 的二维区域,该区域可以直接使用 PaddleScience 内置的空间几何 Rectangle来构造计算域。代码如下

+
79
+80
+81
# set geometry
+NPOINT = cfg.NL * cfg.NW
+geom = {"rect": ppsci.geometry.Rectangle((0, 0), (cfg.DL, cfg.DW))}
+
+
+提示 +

RectangleTimeDomain 是两种可以单独使用的 Geometry 派生类。

+

如输入数据只来自于二维矩形几何域,则可以直接使用 ppsci.geometry.Rectangle(...) 创建空间几何域对象;

+

如输入数据只来自一维时间域,则可以直接使用 ppsci.geometry.TimeDomain(...) 构建时间域对象。

+
+

3.3 输入数据构建

+

使用二维相关且尺度不变的高斯随机场来生成随机热源分布 \(S(x)\) 和边界函数 \(Q(x)\)。我们参考 gaussian-random-fields 中描述的Python实现,其中相关性由无标度谱来解释,即

+
\[ +P(k) \sim \dfrac{1}{|k|^{\alpha/2}}. +\]
+

采样函数的平滑度由长度尺度系数 \(\alpha\) 决定,\(\alpha\) 值越大,得到的随机热源分布 \(S(x)\) 和边界函数 \(Q(x)\) 越平滑。在本文我们采用 \(\alpha = 4\)。还可以调整该参数以生成类似于特定优化任务中的热源分布 \(S(x)\) 和边界函数 \(Q(x)\)

+

通过高斯随机场来生成随机热源分布 \(S(x)\) 和边界函数 \(Q(x)\)的训练和测试输入数据。代码如下

+
84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
# generate training data and validation data
+data_u = np.ones([1, (cfg.NL - 2) * (cfg.NW - 2)])
+data_BC = np.ones([1, NPOINT])
+data_u = np.vstack((data_u, np.zeros([1, (cfg.NL - 2) * (cfg.NW - 2)])))
+data_BC = np.vstack((data_BC, np.zeros([1, NPOINT])))
+for i in range(cfg.NU - 2):
+    data_u = np.vstack((data_u, GRF(alpha=cfg.GRF.alpha, size=cfg.NL - 2)))
+for i in range(cfg.NBC - 2):
+    data_BC = np.vstack((data_BC, GRF(alpha=cfg.GRF.alpha, size=cfg.NL)))
+data_u = data_u.astype("float32")
+data_BC = data_BC.astype("float32")
+test_u = GRF(alpha=4, size=cfg.NL).astype("float32")[0]
+
+

然后对训练数据和测试数据按照空间坐标进行分类,将训练数据和测试数据分类成左边、右边、上边、下边以及内部数据。代码如下

+
boundary_indices = np.where(
+    (
+        (points["x"] == 0)
+        | (points["x"] == cfg.DW)
+        | (points["y"] == 0)
+        | (points["y"] == cfg.DL)
+    )
+)
+interior_indices = np.where(
+    (
+        (points["x"] != 0)
+        & (points["x"] != cfg.DW)
+        & (points["y"] != 0)
+        & (points["y"] != cfg.DL)
+    )
+)
+
+points["u"] = np.tile(test_u[interior_indices[0]], (NPOINT, 1))
+points["u_one"] = test_u.T.reshape([-1, 1])
+points["bc_data"] = np.tile(test_u[boundary_indices[0]], (NPOINT, 1))
+points["bc"] = np.zeros((NPOINT, 1), dtype="float32")
+
+top_indices = np.where(points["x"] == cfg.DW)
+down_indices = np.where(points["x"] == 0)
+left_indices = np.where(
+    (points["y"] == 0) & (points["x"] != 0) & (points["x"] != cfg.DW)
+)
+right_indices = np.where(
+    ((points["y"] == cfg.DL) & (points["x"] != 0) & (points["x"] != cfg.DW))
+)
+
+# generate validation data
+(
+    test_top_data,
+    test_down_data,
+    test_left_data,
+    test_right_data,
+    test_interior_data,
+) = [
+    {
+        "x": points["x"][indices_[0]],
+        "y": points["y"][indices_[0]],
+        "u": points["u"][indices_[0]],
+        "u_one": points["u_one"][indices_[0]],
+        "bc": points["bc"][indices_[0]],
+        "bc_data": points["bc_data"][indices_[0]],
+    }
+    for indices_ in (
+        top_indices,
+        down_indices,
+        left_indices,
+        right_indices,
+        interior_indices,
+    )
+]
+# generate train data
+top_data = {
+    "x": test_top_data["x"],
+    "y": test_top_data["y"],
+    "u": data_u,
+    "u_one": data_BC[:, top_indices[0]].T.reshape([-1, 1]),
+    "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+    "bc_data": data_BC[:, boundary_indices[0]],
+}
+down_data = {
+    "x": test_down_data["x"],
+    "y": test_down_data["y"],
+    "u": data_u,
+    "u_one": data_BC[:, down_indices[0]].T.reshape([-1, 1]),
+    "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+    "bc_data": data_BC[:, boundary_indices[0]],
+}
+left_data = {
+    "x": test_left_data["x"],
+    "y": test_left_data["y"],
+    "u": data_u,
+    "u_one": data_BC[:, left_indices[0]].T.reshape([-1, 1]),
+    "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+    "bc_data": data_BC[:, boundary_indices[0]],
+}
+right_data = {
+    "x": test_right_data["x"],
+    "y": test_right_data["y"],
+    "u": data_u,
+    "u_one": data_BC[:, right_indices[0]].T.reshape([-1, 1]),
+    "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+    "bc_data": data_BC[:, boundary_indices[0]],
+}
+interior_data = {
+    "x": test_interior_data["x"],
+    "y": test_interior_data["y"],
+    "u": data_u,
+    "u_one": data_u.T.reshape([-1, 1]),
+    "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+    "bc_data": data_BC[:, boundary_indices[0]],
+}
+
+

3.4 约束构建

+

在构建约束之前,需要先介绍一下ChipHeatDataset,它继承自 Dataset 类,可以迭代的读取由不同 numpy.ndarray 组成的数组数据集。由于所用的模型分支网数目较多,所用的数据量较大。若先对数据进行组合,将导致输入数据占用的内存很大,因此采用 ChipHeatDataset 迭代读取数据。

+

芯片热仿真问题由 2.1 问题描述 中描述的方程组成,此时我们对左边、右边、上边、下边以及内部数据分别设置五个约束条件,接下来使用 PaddleScience 内置的 SupervisedConstraint 构建上述四种约束条件,代码如下

+
# set constraint
+index = ("x", "u", "bc", "bc_data")
+label = {"chip": np.array([0], dtype="float32")}
+weight = {"chip": np.array([cfg.TRAIN.weight], dtype="float32")}
+top_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ChipHeatDataset",
+            "input": top_data,
+            "label": label,
+            "index": index,
+            "data_type": "bc_data",
+            "weight": weight,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "chip": lambda out: paddle.where(
+            out["bc"] == 1,
+            jacobian(out["T"], out["x"]) - out["u_one"],
+            paddle.where(
+                out["bc"] == 0,
+                out["T"] - out["u_one"],
+                paddle.where(
+                    out["bc"] == 2,
+                    jacobian(out["T"], out["x"]) + out["u_one"] * (out["T"] - 1),
+                    jacobian(out["T"], out["x"])
+                    + out["u_one"]
+                    * (out["T"] ** 2 - 1)
+                    * (out["T"] ** 2 + 1)
+                    * 5.6
+                    / 50000,
+                ),
+            ),
+        )
+    },
+    name="top_sup",
+)
+down_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ChipHeatDataset",
+            "input": down_data,
+            "label": label,
+            "index": index,
+            "data_type": "bc_data",
+            "weight": weight,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "chip": lambda out: paddle.where(
+            out["bc"] == 1,
+            jacobian(out["T"], out["x"]) - out["u_one"],
+            paddle.where(
+                out["bc"] == 0,
+                out["T"] - out["u_one"],
+                paddle.where(
+                    out["bc"] == 2,
+                    jacobian(out["T"], out["x"]) + out["u_one"] * (out["T"] - 1),
+                    jacobian(out["T"], out["x"])
+                    + out["u_one"]
+                    * (out["T"] ** 2 - 1)
+                    * (out["T"] ** 2 + 1)
+                    * 5.6
+                    / 50000,
+                ),
+            ),
+        )
+    },
+    name="down_sup",
+)
+left_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ChipHeatDataset",
+            "input": left_data,
+            "label": label,
+            "index": index,
+            "data_type": "bc_data",
+            "weight": weight,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "chip": lambda out: paddle.where(
+            out["bc"] == 1,
+            jacobian(out["T"], out["y"]) - out["u_one"],
+            paddle.where(
+                out["bc"] == 0,
+                out["T"] - out["u_one"],
+                paddle.where(
+                    out["bc"] == 2,
+                    jacobian(out["T"], out["y"]) + out["u_one"] * (out["T"] - 1),
+                    jacobian(out["T"], out["y"])
+                    + out["u_one"]
+                    * (out["T"] ** 2 - 1)
+                    * (out["T"] ** 2 + 1)
+                    * 5.6
+                    / 50000,
+                ),
+            ),
+        )
+    },
+    name="left_sup",
+)
+right_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ChipHeatDataset",
+            "input": right_data,
+            "label": label,
+            "index": index,
+            "data_type": "bc_data",
+            "weight": weight,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "chip": lambda out: paddle.where(
+            out["bc"] == 1,
+            jacobian(out["T"], out["y"]) - out["u_one"],
+            paddle.where(
+                out["bc"] == 0,
+                out["T"] - out["u_one"],
+                paddle.where(
+                    out["bc"] == 2,
+                    jacobian(out["T"], out["y"]) + out["u_one"] * (out["T"] - 1),
+                    jacobian(out["T"], out["y"])
+                    + out["u_one"]
+                    * (out["T"] ** 2 - 1)
+                    * (out["T"] ** 2 + 1)
+                    * 5.6
+                    / 50000,
+                ),
+            ),
+        )
+    },
+    name="right_sup",
+)
+interior_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ChipHeatDataset",
+            "input": interior_data,
+            "label": label,
+            "index": index,
+            "data_type": "u",
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "chip": lambda out: hessian(out["T"], out["x"])
+        + hessian(out["T"], out["y"])
+        + 100 * out["u_one"]
+    },
+    name="interior_sup",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,其中 “dataset” 字段表示使用的训练数据集信息,各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 ChipHeatDataset 表示分 batch 顺序迭代的读取数据;
  2. +
  3. input: 输入变量名;
  4. +
  5. label: 标签变量名;
  6. +
  7. index: 输入数据集的索引;
  8. +
  9. data_type: 输入数据的类型;
  10. +
  11. weight: 权重大小。
  12. +
+

"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,另外还指定了该类初始化时参数 drop_lastFalseshuffleTrue

+

第二个参数是损失函数,此处我们选用常用的 MSE 函数,且 reduction"mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第三个参数是标签表达式列表,此处我们使用与左边、右边、上边、下边以及内部区域相对应的方程表达式,同时我们分别用 \(0,1,2,3\) 代表Dirichlet边界、Neumann 边界、对流边界以及辐射边界,对与不同的边界类型,设置不同的边界条件;

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在微分方程约束和监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    down_sup_constraint.name: down_sup_constraint,
+    left_sup_constraint.name: left_sup_constraint,
+    right_sup_constraint.name: right_sup_constraint,
+    interior_sup_constraint.name: interior_sup_constraint,
+    top_sup_constraint.name: top_sup_constraint,
+}
+
+

3.5 优化器构建

+

接下来我们需要指定学习率,学习率设为 0.001,训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.6 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,我们使用 ppsci.validate.SupervisedValidator 构建评估器。

+
# set validator
+top_down_label = {"chip": np.zeros([cfg.NL, 1], dtype="float32")}
+left_right_label = {"chip": np.zeros([(cfg.NL - 2), 1], dtype="float32")}
+interior_label = {
+    "thermal_condution": np.zeros(
+        [test_interior_data["x"].shape[0], 1], dtype="float32"
+    )
+}
+top_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_top_data,
+            "label": top_down_label,
+            "weight": {
+                "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+            },
+        },
+        "batch_size": cfg.NL,
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="top_mse",
+)
+down_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_down_data,
+            "label": top_down_label,
+            "weight": {
+                "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+            },
+        },
+        "batch_size": cfg.NL,
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="down_mse",
+)
+left_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_left_data,
+            "label": left_right_label,
+            "weight": {
+                "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+            },
+        },
+        "batch_size": (cfg.NL - 2),
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="left_mse",
+)
+right_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_right_data,
+            "label": left_right_label,
+            "weight": {
+                "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+            },
+        },
+        "batch_size": (cfg.NL - 2),
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="right_mse",
+)
+interior_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_interior_data,
+            "label": interior_label,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "thermal_condution": lambda out: (
+            hessian(out["T"], out["x"]) + hessian(out["T"], out["y"])
+        )
+        + 100 * out["u_one"]
+    },
+    metric={"MSE": ppsci.metric.MSE()},
+    name="interior_mse",
+)
+validator = {
+    down_validator.name: down_validator,
+    left_validator.name: left_validator,
+    right_validator.name: right_validator,
+    top_validator.name: top_validator,
+    interior_validator.name: interior_validator,
+}
+
+

配置与 3.4 约束构建 的设置类似。需要注意的是,由于评估所用的数据量不是很多,因此我们不需要使用ChipHeatDataset 迭代的读取数据,在这里使用NamedArrayDataset 读取数据。

+

3.7 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.8 结果可视化

+

最后在给定的可视化区域上进行预测并可视化,可视化数据是区域内的二维点集,每个坐标 \((x, y)\) 处,对应的温度值 \(T\),在此我们画出 \(T\) 在区域上的变化图像。同时可以根据需要,设置不同的边界类型、随机热源分布 \(S(x)\) 和边界函数 \(Q(x)\),代码如下:

+
# visualize prediction after finished training
+pred_points = geom["rect"].sample_interior(NPOINT, evenly=True)
+pred_points["u"] = points["u"]
+pred_points["bc_data"] = np.zeros_like(points["bc_data"])
+pred_points["bc"] = np.repeat(
+    np.array([[cfg.EVAL.bc_type]], dtype="float32"), NPOINT, axis=0
+)
+pred = solver.predict(pred_points)
+logger.message("Now saving visual result to: visual/result.vtu, please wait...")
+ppsci.visualize.save_vtu_from_dict(
+    osp.join(cfg.output_dir, "visual/result.vtu"),
+    {
+        "x": pred_points["x"],
+        "y": pred_points["y"],
+        "T": pred["T"],
+    },
+    (
+        "x",
+        "y",
+    ),
+    ("T"),
+)
+
+

4. 完整代码

+
chip_heat.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
+693
+694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
+740
+741
+742
+743
+744
+745
+746
+747
+748
+749
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+import scipy.fftpack
+import scipy.io
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import hessian
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+
+def fftind(size):
+    """
+    Returns the momentum indices for the 2D Fast Fourier Transform (FFT).
+
+    Args:
+        size (int): Size of the 2D array.
+
+    Returns:
+        numpy.ndarray: Array of momentum indices for the 2D FFT.
+    """
+    k_ind = np.mgrid[:size, :size] - int((size + 1) / 2)
+    k_ind = scipy.fftpack.fftshift(k_ind)
+    return k_ind
+
+
+def GRF(alpha=3.0, size=128, flag_normalize=True):
+    """
+    Generates a Gaussian random field(GRF) with a power law amplitude spectrum.
+
+    Args:
+        alpha (float, optional): Power law exponent. Defaults to 3.0.
+        size (int, optional): Size of the output field. Defaults to 128.
+        flag_normalize (bool, optional): Flag indicating whether to normalize the field. Defaults to True.
+
+    Returns:
+        numpy.ndarray: Generated Gaussian random field.
+    """
+    # Defines momentum indices
+    k_idx = fftind(size)
+    # Defines the amplitude as a power law 1/|k|^(alpha/2)
+    amplitude = np.power(k_idx[0] ** 2 + k_idx[1] ** 2 + 1e-10, -alpha / 4.0)
+    amplitude[0, 0] = 0
+    # Draws a complex gaussian random noise with normal
+    # (circular) distribution
+    noise = np.random.normal(size=(size, size)) + 1j * np.random.normal(
+        size=(size, size)
+    )
+    # To real space
+    gfield = np.fft.ifft2(noise * amplitude).real
+    # Sets the standard deviation to one
+    if flag_normalize:
+        gfield = gfield - np.mean(gfield)
+        gfield = gfield / np.std(gfield)
+    return gfield.reshape([1, -1])
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.ChipDeepONets(**cfg.MODEL)
+    # set geometry
+    NPOINT = cfg.NL * cfg.NW
+    geom = {"rect": ppsci.geometry.Rectangle((0, 0), (cfg.DL, cfg.DW))}
+    points = geom["rect"].sample_interior(NPOINT, evenly=True)
+
+    # generate training data and validation data
+    data_u = np.ones([1, (cfg.NL - 2) * (cfg.NW - 2)])
+    data_BC = np.ones([1, NPOINT])
+    data_u = np.vstack((data_u, np.zeros([1, (cfg.NL - 2) * (cfg.NW - 2)])))
+    data_BC = np.vstack((data_BC, np.zeros([1, NPOINT])))
+    for i in range(cfg.NU - 2):
+        data_u = np.vstack((data_u, GRF(alpha=cfg.GRF.alpha, size=cfg.NL - 2)))
+    for i in range(cfg.NBC - 2):
+        data_BC = np.vstack((data_BC, GRF(alpha=cfg.GRF.alpha, size=cfg.NL)))
+    data_u = data_u.astype("float32")
+    data_BC = data_BC.astype("float32")
+    test_u = GRF(alpha=4, size=cfg.NL).astype("float32")[0]
+
+    boundary_indices = np.where(
+        (
+            (points["x"] == 0)
+            | (points["x"] == cfg.DW)
+            | (points["y"] == 0)
+            | (points["y"] == cfg.DL)
+        )
+    )
+    interior_indices = np.where(
+        (
+            (points["x"] != 0)
+            & (points["x"] != cfg.DW)
+            & (points["y"] != 0)
+            & (points["y"] != cfg.DL)
+        )
+    )
+
+    points["u"] = np.tile(test_u[interior_indices[0]], (NPOINT, 1))
+    points["u_one"] = test_u.T.reshape([-1, 1])
+    points["bc_data"] = np.tile(test_u[boundary_indices[0]], (NPOINT, 1))
+    points["bc"] = np.zeros((NPOINT, 1), dtype="float32")
+
+    top_indices = np.where(points["x"] == cfg.DW)
+    down_indices = np.where(points["x"] == 0)
+    left_indices = np.where(
+        (points["y"] == 0) & (points["x"] != 0) & (points["x"] != cfg.DW)
+    )
+    right_indices = np.where(
+        ((points["y"] == cfg.DL) & (points["x"] != 0) & (points["x"] != cfg.DW))
+    )
+
+    # generate validation data
+    (
+        test_top_data,
+        test_down_data,
+        test_left_data,
+        test_right_data,
+        test_interior_data,
+    ) = [
+        {
+            "x": points["x"][indices_[0]],
+            "y": points["y"][indices_[0]],
+            "u": points["u"][indices_[0]],
+            "u_one": points["u_one"][indices_[0]],
+            "bc": points["bc"][indices_[0]],
+            "bc_data": points["bc_data"][indices_[0]],
+        }
+        for indices_ in (
+            top_indices,
+            down_indices,
+            left_indices,
+            right_indices,
+            interior_indices,
+        )
+    ]
+    # generate train data
+    top_data = {
+        "x": test_top_data["x"],
+        "y": test_top_data["y"],
+        "u": data_u,
+        "u_one": data_BC[:, top_indices[0]].T.reshape([-1, 1]),
+        "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+        "bc_data": data_BC[:, boundary_indices[0]],
+    }
+    down_data = {
+        "x": test_down_data["x"],
+        "y": test_down_data["y"],
+        "u": data_u,
+        "u_one": data_BC[:, down_indices[0]].T.reshape([-1, 1]),
+        "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+        "bc_data": data_BC[:, boundary_indices[0]],
+    }
+    left_data = {
+        "x": test_left_data["x"],
+        "y": test_left_data["y"],
+        "u": data_u,
+        "u_one": data_BC[:, left_indices[0]].T.reshape([-1, 1]),
+        "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+        "bc_data": data_BC[:, boundary_indices[0]],
+    }
+    right_data = {
+        "x": test_right_data["x"],
+        "y": test_right_data["y"],
+        "u": data_u,
+        "u_one": data_BC[:, right_indices[0]].T.reshape([-1, 1]),
+        "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+        "bc_data": data_BC[:, boundary_indices[0]],
+    }
+    interior_data = {
+        "x": test_interior_data["x"],
+        "y": test_interior_data["y"],
+        "u": data_u,
+        "u_one": data_u.T.reshape([-1, 1]),
+        "bc": np.array([[0], [1], [2], [3]], dtype="float32"),
+        "bc_data": data_BC[:, boundary_indices[0]],
+    }
+
+    # set constraint
+    index = ("x", "u", "bc", "bc_data")
+    label = {"chip": np.array([0], dtype="float32")}
+    weight = {"chip": np.array([cfg.TRAIN.weight], dtype="float32")}
+    top_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "ChipHeatDataset",
+                "input": top_data,
+                "label": label,
+                "index": index,
+                "data_type": "bc_data",
+                "weight": weight,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "chip": lambda out: paddle.where(
+                out["bc"] == 1,
+                jacobian(out["T"], out["x"]) - out["u_one"],
+                paddle.where(
+                    out["bc"] == 0,
+                    out["T"] - out["u_one"],
+                    paddle.where(
+                        out["bc"] == 2,
+                        jacobian(out["T"], out["x"]) + out["u_one"] * (out["T"] - 1),
+                        jacobian(out["T"], out["x"])
+                        + out["u_one"]
+                        * (out["T"] ** 2 - 1)
+                        * (out["T"] ** 2 + 1)
+                        * 5.6
+                        / 50000,
+                    ),
+                ),
+            )
+        },
+        name="top_sup",
+    )
+    down_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "ChipHeatDataset",
+                "input": down_data,
+                "label": label,
+                "index": index,
+                "data_type": "bc_data",
+                "weight": weight,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "chip": lambda out: paddle.where(
+                out["bc"] == 1,
+                jacobian(out["T"], out["x"]) - out["u_one"],
+                paddle.where(
+                    out["bc"] == 0,
+                    out["T"] - out["u_one"],
+                    paddle.where(
+                        out["bc"] == 2,
+                        jacobian(out["T"], out["x"]) + out["u_one"] * (out["T"] - 1),
+                        jacobian(out["T"], out["x"])
+                        + out["u_one"]
+                        * (out["T"] ** 2 - 1)
+                        * (out["T"] ** 2 + 1)
+                        * 5.6
+                        / 50000,
+                    ),
+                ),
+            )
+        },
+        name="down_sup",
+    )
+    left_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "ChipHeatDataset",
+                "input": left_data,
+                "label": label,
+                "index": index,
+                "data_type": "bc_data",
+                "weight": weight,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "chip": lambda out: paddle.where(
+                out["bc"] == 1,
+                jacobian(out["T"], out["y"]) - out["u_one"],
+                paddle.where(
+                    out["bc"] == 0,
+                    out["T"] - out["u_one"],
+                    paddle.where(
+                        out["bc"] == 2,
+                        jacobian(out["T"], out["y"]) + out["u_one"] * (out["T"] - 1),
+                        jacobian(out["T"], out["y"])
+                        + out["u_one"]
+                        * (out["T"] ** 2 - 1)
+                        * (out["T"] ** 2 + 1)
+                        * 5.6
+                        / 50000,
+                    ),
+                ),
+            )
+        },
+        name="left_sup",
+    )
+    right_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "ChipHeatDataset",
+                "input": right_data,
+                "label": label,
+                "index": index,
+                "data_type": "bc_data",
+                "weight": weight,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "chip": lambda out: paddle.where(
+                out["bc"] == 1,
+                jacobian(out["T"], out["y"]) - out["u_one"],
+                paddle.where(
+                    out["bc"] == 0,
+                    out["T"] - out["u_one"],
+                    paddle.where(
+                        out["bc"] == 2,
+                        jacobian(out["T"], out["y"]) + out["u_one"] * (out["T"] - 1),
+                        jacobian(out["T"], out["y"])
+                        + out["u_one"]
+                        * (out["T"] ** 2 - 1)
+                        * (out["T"] ** 2 + 1)
+                        * 5.6
+                        / 50000,
+                    ),
+                ),
+            )
+        },
+        name="right_sup",
+    )
+    interior_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "ChipHeatDataset",
+                "input": interior_data,
+                "label": label,
+                "index": index,
+                "data_type": "u",
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "chip": lambda out: hessian(out["T"], out["x"])
+            + hessian(out["T"], out["y"])
+            + 100 * out["u_one"]
+        },
+        name="interior_sup",
+    )
+    # wrap constraints together
+    constraint = {
+        down_sup_constraint.name: down_sup_constraint,
+        left_sup_constraint.name: left_sup_constraint,
+        right_sup_constraint.name: right_sup_constraint,
+        interior_sup_constraint.name: interior_sup_constraint,
+        top_sup_constraint.name: top_sup_constraint,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    top_down_label = {"chip": np.zeros([cfg.NL, 1], dtype="float32")}
+    left_right_label = {"chip": np.zeros([(cfg.NL - 2), 1], dtype="float32")}
+    interior_label = {
+        "thermal_condution": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        )
+    }
+    top_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_top_data,
+                "label": top_down_label,
+                "weight": {
+                    "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": cfg.NL,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="top_mse",
+    )
+    down_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_down_data,
+                "label": top_down_label,
+                "weight": {
+                    "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": cfg.NL,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="down_mse",
+    )
+    left_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_left_data,
+                "label": left_right_label,
+                "weight": {
+                    "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": (cfg.NL - 2),
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="left_mse",
+    )
+    right_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_right_data,
+                "label": left_right_label,
+                "weight": {
+                    "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": (cfg.NL - 2),
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="right_mse",
+    )
+    interior_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_interior_data,
+                "label": interior_label,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "thermal_condution": lambda out: (
+                hessian(out["T"], out["x"]) + hessian(out["T"], out["y"])
+            )
+            + 100 * out["u_one"]
+        },
+        metric={"MSE": ppsci.metric.MSE()},
+        name="interior_mse",
+    )
+    validator = {
+        down_validator.name: down_validator,
+        left_validator.name: left_validator,
+        right_validator.name: right_validator,
+        top_validator.name: top_validator,
+        interior_validator.name: interior_validator,
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    pred_points = geom["rect"].sample_interior(NPOINT, evenly=True)
+    pred_points["u"] = points["u"]
+    pred_points["bc_data"] = np.zeros_like(points["bc_data"])
+    pred_points["bc"] = np.repeat(
+        np.array([[cfg.EVAL.bc_type]], dtype="float32"), NPOINT, axis=0
+    )
+    pred = solver.predict(pred_points)
+    logger.message("Now saving visual result to: visual/result.vtu, please wait...")
+    ppsci.visualize.save_vtu_from_dict(
+        osp.join(cfg.output_dir, "visual/result.vtu"),
+        {
+            "x": pred_points["x"],
+            "y": pred_points["y"],
+            "T": pred["T"],
+        },
+        (
+            "x",
+            "y",
+        ),
+        ("T"),
+    )
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.ChipDeepONets(**cfg.MODEL)
+    # set geometry
+    NPOINT = cfg.NL * cfg.NW
+    geom = {"rect": ppsci.geometry.Rectangle((0, 0), (cfg.DL, cfg.DW))}
+    points = geom["rect"].sample_interior(NPOINT, evenly=True)
+
+    # generate validation data
+    test_u = GRF(alpha=4, size=cfg.NL).astype("float32")[0]
+
+    boundary_indices = np.where(
+        (
+            (points["x"] == 0)
+            | (points["x"] == cfg.DW)
+            | (points["y"] == 0)
+            | (points["y"] == cfg.DL)
+        )
+    )
+    interior_indices = np.where(
+        (
+            (points["x"] != 0)
+            & (points["x"] != cfg.DW)
+            & (points["y"] != 0)
+            & (points["y"] != cfg.DL)
+        )
+    )
+
+    points["u"] = np.tile(test_u[interior_indices[0]], (NPOINT, 1))
+    points["u_one"] = test_u.T.reshape([-1, 1])
+    points["bc_data"] = np.tile(test_u[boundary_indices[0]], (NPOINT, 1))
+    points["bc"] = np.zeros((NPOINT, 1), dtype="float32")
+
+    top_indices = np.where(points["x"] == cfg.DW)
+    down_indices = np.where(points["x"] == 0)
+    left_indices = np.where(
+        (points["y"] == 0) & (points["x"] != 0) & (points["x"] != cfg.DW)
+    )
+    right_indices = np.where(
+        ((points["y"] == cfg.DL) & (points["x"] != 0) & (points["x"] != cfg.DW))
+    )
+
+    # generate validation data
+    (
+        test_top_data,
+        test_down_data,
+        test_left_data,
+        test_right_data,
+        test_interior_data,
+    ) = [
+        {
+            "x": points["x"][indices_[0]],
+            "y": points["y"][indices_[0]],
+            "u": points["u"][indices_[0]],
+            "u_one": points["u_one"][indices_[0]],
+            "bc": points["bc"][indices_[0]],
+            "bc_data": points["bc_data"][indices_[0]],
+        }
+        for indices_ in (
+            top_indices,
+            down_indices,
+            left_indices,
+            right_indices,
+            interior_indices,
+        )
+    ]
+
+    # set validator
+    top_down_label = {"chip": np.zeros([cfg.NL, 1], dtype="float32")}
+    left_right_label = {"chip": np.zeros([(cfg.NL - 2), 1], dtype="float32")}
+    interior_label = {
+        "thermal_condution": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        )
+    }
+    top_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_top_data,
+                "label": top_down_label,
+                "weight": {
+                    "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": cfg.NL,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="top_mse",
+    )
+    down_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_down_data,
+                "label": top_down_label,
+                "weight": {
+                    "chip": np.full([cfg.NL, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": cfg.NL,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="down_mse",
+    )
+    left_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_left_data,
+                "label": left_right_label,
+                "weight": {
+                    "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": (cfg.NL - 2),
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="left_mse",
+    )
+    right_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_right_data,
+                "label": left_right_label,
+                "weight": {
+                    "chip": np.full([cfg.NL - 2, 1], cfg.TRAIN.weight, dtype="float32")
+                },
+            },
+            "batch_size": (cfg.NL - 2),
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"chip": lambda out: out["T"] - out["u_one"]},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="right_mse",
+    )
+    interior_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_interior_data,
+                "label": interior_label,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "thermal_condution": lambda out: (
+                hessian(out["T"], out["x"]) + hessian(out["T"], out["y"])
+            )
+            + 100 * out["u_one"]
+        },
+        metric={"MSE": ppsci.metric.MSE()},
+        name="interior_mse",
+    )
+    validator = {
+        down_validator.name: down_validator,
+        left_validator.name: left_validator,
+        right_validator.name: right_validator,
+        top_validator.name: top_validator,
+        interior_validator.name: interior_validator,
+    }
+
+    # directly evaluate pretrained model(optional)
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction result
+    pred_points = geom["rect"].sample_interior(NPOINT, evenly=True)
+    pred_points["u"] = points["u"]
+    pred_points["bc_data"] = np.zeros_like(points["bc_data"])
+    pred_points["bc"] = np.full((NPOINT, 1), cfg.EVAL.bc_type, dtype="float32")
+    pred = solver.predict(pred_points)
+    logger.message("Now saving visual result to: visual/result.vtu, please wait...")
+    ppsci.visualize.save_vtu_from_dict(
+        osp.join(cfg.output_dir, "visual/result.vtu"),
+        {
+            "x": pred_points["x"],
+            "y": pred_points["y"],
+            "T": pred["T"],
+        },
+        (
+            "x",
+            "y",
+        ),
+        ("T"),
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="chip_heat.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

通过高斯随机场生成三组随机热源分布 \(S(x)\),如图中第一行所示。接下来我们可以设置第一类 PDE 中的任意边界条件,在此我们给出了五类边界条件,如图中第一列控制方程中边界方程所示,在测试过程中,我们设 \(k = 100,~h = 100,~T_{amb} = 1,~\epsilon\sigma= 5.6 \times 10^{-7}\)。 在不同随机热源 \(S(x)\) 分布和不同边界条件下,我们通过 PI-DeepONet 模型测试的温度场分布如图所示。从图中可知,尽管随机热源分布 \(S(x)\) 和边界条件在测试样本之间存在着显着差异,但 PI-DeepONet 模型均可以正确预测由热传导方程控制的内部和边界上的二维扩散性质解。

+
+

chip.png

+
+

6. 参考资料

+

参考文献: A fast general thermal simulation model based on MultiBranch Physics-Informed deep operator neural network

+

参考代码: gaussian-random-fields

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/control_arm/index.html b/zh/examples/control_arm/index.html new file mode 100644 index 0000000000..6e81df574a --- /dev/null +++ b/zh/examples/control_arm/index.html @@ -0,0 +1,6673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Control_arm - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Control arm

+ + +
+
+
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python forward_analysis.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python inverse_parameter.py TRAIN.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/forward_x_axis_pretrained.pdparams
+
+
+
+
+
+
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python forward_analysis.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/forward_x_axis_pretrained.pdparams
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python inverse_parameter.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/control_arm/inverse_x_axis_pretrained.pdparams
+
+
+
+
+
+
+
+
+
+
python forward_analysis.py mode=export
+
+
+
+
python inverse_parameter.py mode=export
+
+
+
+
+
+
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python forward_analysis.py mode=infer
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/control_arm/control_arm.stl --create-dirs -o ./datasets/control_arm.stl
+python inverse_parameter.py mode=infer
+
+
+
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
inverse_x_axis_pretrained.pdparamsloss(geo_eval): 0.02505
L2Rel.lambda_(geo_eval): 0.06025
L2Rel.mu(geo_eval): 0.07949
+

1. 背景简介

+

结构受力分析是在符合某个边界条件的结构受到特定条件的载荷后,结构会产生相应的应力应变,此时对它们状态的分析。 应力是一个物理量,用于描述物体内部由于外力而产生的单位面积上的力。应变则描述了物体的形状和尺寸的变化。 通常结构力学问题分为静力学问题和动力学问题,本案例着眼于静力学分析,即结构达到受力平衡状态后再进行分析。 本问题假设结构受到一个比较小的力,此时结构形变符合线弹性方程。

+

需要指出的是,能够适用线弹性方程的结构需要满足在受力后能够完全恢复原状,即没有永久变形。 这种假设在很多情况下是合理的,但同时对于某些可能产生永久形变的材料(如塑料或橡胶)来说,这种假设可能不准确。 要全面理解形变,还需要考虑其他因素,例如物体的初始形状和尺寸、外力的历史、材料的其他物理性质(如热膨胀系数和密度)等。

+

汽车控制臂,也称为悬挂臂或悬挂控制臂,是连接车轮和车辆底盘的重要零件。控制臂作为汽车悬架系统的导向和传力元件,将作用在车轮上的各种力传递给车身,同时保证车轮按一定轨迹运动。控制臂分别通过球铰或者衬套把车轮和车身弹性地连接在一起 +,控制臂(包括与之相连的衬套及球头)应有足够的刚度、强度和使用寿命。

+

本问题主要研究如下汽车悬挂控制臂结构上的受力分析情况以及验证在不给定附加数据的情况下进行参数逆推的可能性,并使用深度学习方法根据线弹性等方程进行求解,结构如下所示,左侧单一圆环内表面受力,右侧两圆环内表面固定,共研究了受力方向为:x 轴负方向、z 轴正方向两种情况,下面以 x 轴正方向受力为例进行说明。

+
+

control_arm +

+
控制臂结构示意图
+
+

2. 问题定义

+

线弹性方程是描述物体在受力后恢复原状的能力的数学模型,表征为应力和应变之间的线性关系,其中系数被称为弹性模量(或杨氏模量),它的公式为:

+
\[ +\begin{cases} + stress\_disp_{xx} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial u}{\partial x} - \sigma_{xx} \\ + stress\_disp_{yy} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial v}{\partial y} - \sigma_{yy} \\ + stress\_disp_{zz} = \lambda(\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} + \dfrac{\partial w}{\partial z}) + 2\mu \dfrac{\partial w}{\partial z} - \sigma_{zz} \\ + traction_{x} = n_x \sigma_{xx} + n_y \sigma_{xy} + n_z \sigma_{xz} \\ + traction_{y} = n_y \sigma_{yx} + n_y \sigma_{yy} + n_z \sigma_{yz} \\ + traction_{z} = n_z \sigma_{zx} + n_y \sigma_{zy} + n_z \sigma_{zz} \\ +\end{cases} +\]
+

其中 \((x,y,z)\) 为输入的位置坐标点,\((u,v,w)\) 为对应坐标点三个维度上的应变,\((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) 为 对应坐标点三个维度上的应力。

+

结构左侧圆环内表面受到受到均匀分布的,沿 z 轴正方向大小为 \(0.0025\) 的均匀应力。其它参数包括弹性模量 \(E=1\),泊松比 \(\nu=0.3\)。目标求解该金属件表面每个点的 \(u\)\(v\)\(w\)\(\sigma_{xx}\)\(\sigma_{yy}\)\(\sigma_{zz}\)\(\sigma_{xy}\)\(\sigma_{xz}\)\(\sigma_{yz}\) 共 9 个物理量。常量定义代码如下:

+
28
+29
+30
+31
+32
log_freq: 100
+
+# set working condition
+NU: 0.3
+E: 1
+
+
32
+33
+34
# specify parameters
+LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))
+MU = cfg.E / (2 * (1 + cfg.NU))
+
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 受力分析求解

+

3.1.1 模型构建

+

如上所述,每一个已知的坐标点 \((x, y, z)\) 都有对应的待求解的未知量:三个方向的应变 \((u, v, w)\) 和应力 \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\)

+

考虑到两组物理量对应着不同的方程,因此使用两个模型来分别预测这两组物理量:

+
\[ +\begin{cases} +u, v, w = f(x,y,z) \\ +\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz} = g(x,y,z) +\end{cases} +\]
+

上式中 \(f\) 即为应变模型 disp_net\(g\) 为应力模型 stress_net,因为两者共享输入,因此在 PaddleScience 分别定义这两个网络模型后,再使用 ppsci.arch.ModelList 进行封装,用 PaddleScience 代码表示如下:

+
20
+21
+22
+23
+24
# set model
+disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+# wrap to a model_list
+model_list = ppsci.arch.ModelList((disp_net, stress_net))
+
+

3.1.2 方程构建

+

线弹性方程使用 PaddleScience 内置的 LinearElasticity 即可。

+
36
+37
+38
+39
+40
+41
# set equation
+equation = {
+    "LinearElasticity": ppsci.equation.LinearElasticity(
+        E=None, nu=None, lambda_=LAMBDA_, mu=MU, dim=3
+    )
+}
+
+

3.1.3 计算域构建

+

本问题的几何区域由 stl 文件指定,按照本文档起始处"模型训练命令"下载并解压到 ./datasets/ 文件夹下。

+

注:数据集中的 stl 文件来自网络

+
+注意 +

使用 Mesh 类之前,必须先按照1.4.2 额外依赖安装[可选]文档,安装好 open3d、pysdf、PyMesh 3 个几何依赖包。

+
+

然后通过 PaddleScience 内置的 STL 几何类 ppsci.geometry.Mesh 即可读取、解析几何文件,得到计算域,并获取几何结构边界:

+
43
+44
+45
+46
+47
# set geometry
+control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+geom = {"geo": control_arm}
+# set bounds
+BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+

3.1.4 超参数设定

+

接下来需要在配置文件中指定训练轮数,此处按实验经验,使用 2000 轮训练轮数,每轮进行 1000 步优化。

+
# training settings
+TRAIN:
+
+

3.1.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并配合使用机器学习中常用的 ExponentialDecay 学习率调整策略。

+
26
+27
+28
+29
+30
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model_list)
+
+

3.1.6 约束构建

+

本问题共涉及到 4 个约束,分别为左侧圆环内表面受力的约束、右侧两圆环内表面固定的约束、结构表面边界条件的约束和结构内部点的约束。在具体约束构建之前,可以先构建数据读取配置,以便后续构建多个约束时复用该配置。

+
49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
# set dataloader config
+train_dataloader_cfg = {
+    "dataset": "NamedArrayDataset",
+    "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "num_workers": 1,
+}
+
+
3.1.6.1 内部点约束
+

以作用在结构内部点的 InteriorConstraint 为例,代码如下:

+
arm_interior_constraint = ppsci.constraint.InteriorConstraint(
+    equation["LinearElasticity"].equations,
+    {
+        "equilibrium_x": 0,
+        "equilibrium_y": 0,
+        "equilibrium_z": 0,
+        "stress_disp_xx": 0,
+        "stress_disp_yy": 0,
+        "stress_disp_zz": 0,
+        "stress_disp_xy": 0,
+        "stress_disp_xz": 0,
+        "stress_disp_yz": 0,
+    },
+    geom["geo"],
+    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_interior},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda x, y, z: (
+        (BOUNDS_X[0] < x)
+        & (x < BOUNDS_X[1])
+        & (BOUNDS_Y[0] < y)
+        & (y < BOUNDS_Y[1])
+        & (BOUNDS_Z[0] < z)
+        & (z < BOUNDS_Z[1])
+    ),
+    weight_dict={
+        "equilibrium_x": "sdf",
+        "equilibrium_y": "sdf",
+        "equilibrium_z": "sdf",
+        "stress_disp_xx": "sdf",
+        "stress_disp_yy": "sdf",
+        "stress_disp_zz": "sdf",
+        "stress_disp_xy": "sdf",
+        "stress_disp_xz": "sdf",
+        "stress_disp_yz": "sdf",
+    },
+    name="INTERIOR",
+)
+
+

InteriorConstraint 的第一个参数是方程(组)表达式,用于描述如何计算约束目标,此处填入在 3.1.2 方程构建 章节中实例化好的 equation["LinearElasticity"].equations

+

第二个参数是约束变量的目标值,在本问题中希望与 LinearElasticity 方程相关的 9 个值 equilibrium_x, equilibrium_y, equilibrium_z, stress_disp_xx, stress_disp_yy, stress_disp_zz, stress_disp_xy, stress_disp_xz, stress_disp_yz 均被优化至 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.1.3 计算域构建 章节实例化好的 geom["geo"] 即可;

+

第四个参数是在计算域上的采样配置,此处设置 batch_size 为:

+
arm_right: 256
+
+

第五个参数是损失函数,此处选用常用的 MSE 函数,且 reduction 设置为 "sum",即会将参与计算的所有数据点产生的损失项求和;

+

第六个参数是几何点筛选,需要对 geo 上采样出的点进行筛选,此处传入一个 lambda 筛选函数即可,其接受点集构成的张量 x, y, z,返回布尔值张量,表示每个点是否符合筛选条件,不符合为 False,符合为 True,因为本案例结构来源于网络,参数不完全精确,因此增加 1e-1 作为可容忍的采样误差。

+

第七个参数是每个点参与损失计算时的权重,此处我们使用 "sdf" 表示使用每个点到边界的最短距离(符号距离函数值)来作为权重,这种 sdf 加权的方法可以加大远离边界(难样本)点的权重,减少靠近边界的(简单样本)点的权重,有利于提升模型的精度和收敛速度。

+

第八个参数是约束条件的名字,需要给每一个约束条件命名,方便后续对其索引。此处命名为 "INTERIOR" 即可。

+
3.1.6.2 边界约束
+

结构左侧圆环内表面受力,其上的每个点受到均匀分布的载荷,参照 2. 问题定义,大小存放在参数 \(T\) 中。实际上为 x 负方向的载荷,大小为 \(0.0025\),其余方向应力为 0,有如下边界条件约束:

+
62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
arm_left_constraint = ppsci.constraint.BoundaryConstraint(
+    equation["LinearElasticity"].equations,
+    {"traction_x": cfg.T[0], "traction_y": cfg.T[1], "traction_z": cfg.T[2]},
+    geom["geo"],
+    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_left},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda x, y, z: np.sqrt(
+        np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
+        + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
+    )
+    <= cfg.CIRCLE_LEFT_RADIUS + 1e-1,
+    name="BC_LEFT",
+)
+
+

结构右侧两圆环内表面固定,所以其上的点在三个方向的形变均为 0,因此有如下的边界约束条件:

+
75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
arm_right_constraint = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
+    {"u": 0, "v": 0, "w": 0},
+    geom["geo"],
+    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_right},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda x, y, z: np.sqrt(
+        np.square(x - cfg.CIRCLE_RIGHT_CENTER_XZ[0])
+        + np.square(z - cfg.CIRCLE_RIGHT_CENTER_XZ[1])
+    )
+    <= cfg.CIRCLE_RIGHT_RADIUS + 1e-1,
+    weight_dict=cfg.TRAIN.weight.arm_right,
+    name="BC_RIGHT",
+)
+
+

结构表面不受任何载荷,即三个方向的内力平衡,合力为 0,有如下边界条件约束:

+
arm_surface_constraint = ppsci.constraint.BoundaryConstraint(
+    equation["LinearElasticity"].equations,
+    {"traction_x": 0, "traction_y": 0, "traction_z": 0},
+    geom["geo"],
+    {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_surface},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda x, y, z: np.sqrt(
+        np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
+        + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
+    )
+    > cfg.CIRCLE_LEFT_RADIUS + 1e-1,
+    name="BC_SURFACE",
+)
+
+

在方程约束、边界约束构建完毕之后,以刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints togetherg
+constraint = {
+    arm_left_constraint.name: arm_left_constraint,
+    arm_right_constraint.name: arm_right_constraint,
+    arm_surface_constraint.name: arm_surface_constraint,
+    arm_interior_constraint.name: arm_interior_constraint,
+}
+
+

3.1.7 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,可以选择合适的可视化器来对输出结果进行可视化。

+

可视化器的输入数据通过调用 PaddleScience 的 API sample_interior 产生,输出数据是对应的 9 个预测的物理量,通过设置 ppsci.visualize.VisualizerVtu ,可以将评估的输出数据保存成 vtu格式 文件,最后用可视化软件打开查看即可。

+
# set visualizer(optional)
+# add inferencer data
+samples = geom["geo"].sample_interior(
+    cfg.TRAIN.batch_size.visualizer_vtu,
+    criteria=lambda x, y, z: (
+        (BOUNDS_X[0] < x)
+        & (x < BOUNDS_X[1])
+        & (BOUNDS_Y[0] < y)
+        & (y < BOUNDS_Y[1])
+        & (BOUNDS_Z[0] < z)
+        & (z < BOUNDS_Z[1])
+    ),
+)
+pred_input_dict = {
+    k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+}
+visualizer = {
+    "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
+        pred_input_dict,
+        {
+            "u": lambda out: out["u"],
+            "v": lambda out: out["v"],
+            "w": lambda out: out["w"],
+            "sigma_xx": lambda out: out["sigma_xx"],
+            "sigma_yy": lambda out: out["sigma_yy"],
+            "sigma_zz": lambda out: out["sigma_zz"],
+            "sigma_xy": lambda out: out["sigma_xy"],
+            "sigma_xz": lambda out: out["sigma_xz"],
+            "sigma_yz": lambda out: out["sigma_yz"],
+        },
+        prefix="vis",
+    )
+}
+
+

3.1.8 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model_list,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    seed=cfg.seed,
+    equation=equation,
+    geom=geom,
+    save_freq=cfg.TRAIN.save_freq,
+    log_freq=cfg.log_freq,
+    eval_freq=cfg.TRAIN.eval_freq,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+    visualizer=visualizer,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+
+# train model
+solver.train()
+
+

训练后调用 ppsci.solver.Solver.plot_loss_history 可以将训练中的 loss 画出:

+
# plot losses
+solver.plot_loss_history(by_epoch=True, smooth_step=1)
+
+

另外本案例中提供了并行训练的设置,注意打开数据并行后,应该将学习率应该增大为 原始学习率*并行卡数,以保证训练效果。具体细节请参考 使用指南 2.1.1 数据并行

+
# set parallel
+enable_parallel = dist.get_world_size() > 1
+
+
# re-assign to cfg.TRAIN.iters_per_epoch
+if enable_parallel:
+    cfg.TRAIN.iters_per_epoch = len(arm_left_constraint.data_loader)
+
+

3.1.9 模型评估与可视化

+

训练完成或下载预训练模型后,通过本文档起始处“模型评估命令”进行模型评估和可视化。

+

评估和可视化过程不需要进行优化器等构建,仅需构建模型、计算域、评估器(本案例不包括)、可视化器,然后按顺序传递给 ppsci.solver.Solver 启动评估和可视化。

+
def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    # wrap to a model_list
+    model_list = ppsci.arch.ModelList((disp_net, stress_net))
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.TRAIN.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "u": lambda out: out["u"],
+                "v": lambda out: out["v"],
+                "w": lambda out: out["w"],
+                "sigma_xx": lambda out: out["sigma_xx"],
+                "sigma_yy": lambda out: out["sigma_yy"],
+                "sigma_zz": lambda out: out["sigma_zz"],
+                "sigma_xy": lambda out: out["sigma_xy"],
+                "sigma_xz": lambda out: out["sigma_xz"],
+                "sigma_yz": lambda out: out["sigma_yz"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        geom=geom,
+        log_freq=cfg.log_freq,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+
+    # visualize prediction after finished training
+    solver.visualize()
+
+

3.2 参数逆推求解

+

3.2.1 模型构建

+

进行参数逆推的前提是需要知道每一个已知的坐标点 \((x, y, z)\) ,以及对应的三个方向的应变 \((u, v, w)\) 和应力 \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\)。这些变量的来源可以是真实数据,或数值模拟数据,或已经训练好的正问题模型。在本案例中,我们不使用任何数据,而是使用 3.1 受力分析求解 章节中训练得到的模型来获取这些变量,因此仍然需要构建这部分模型,并为 disp_netstress_net 加载正问题求解得到的权重参数作为预训练模型,注意将这两个模型冻结,以减少反向传播时间和内存占用。

+

参数逆推中需要求解两个未知量:线弹性方程的参数 \(\lambda\)\(\mu\),使用两个模型来分别预测这两组物理量:

+
\[ +\begin{cases} +\lambda = f(x,y,z) \\ +\mu = g(x,y,z) +\end{cases} +\]
+

上式中 \(f\) 即为求解 \(\lambda\) 的模型 inverse_lambda_net\(g\) 为求解 \(\mu\) 模型 inverse_mu_net

+

因为上述两个模型与disp_netstress_net 共四个模型共享输入,因此在 PaddleScience 分别定义这四个网络模型后,再使用 ppsci.arch.ModelList 进行封装,用 PaddleScience 代码表示如下:

+
16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
# set model
+disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
+inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
+# freeze models
+disp_net.freeze()
+stress_net.freeze()
+# wrap to a model_list
+model = ppsci.arch.ModelList(
+    (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
+)
+
+

3.2.2 方程构建

+

线弹性方程使用 PaddleScience 内置的 LinearElasticity 即可。

+
35
+36
+37
+38
+39
+40
# set equation
+equation = {
+    "LinearElasticity": ppsci.equation.LinearElasticity(
+        E=None, nu=None, lambda_="lambda_", mu="mu", dim=3
+    )
+}
+
+

3.2.3 计算域构建

+

本问题的几何区域由 stl 文件指定,按照本文档起始处"模型训练命令"下载并解压到 ./datasets/ 文件夹下。

+

注:数据集中的 stl 文件来自网络

+
+注意 +

使用 Mesh 类之前,必须先按照1.4.2 额外依赖安装[可选]文档,安装好 open3d、pysdf、PyMesh 3 个几何依赖包。

+
+

然后通过 PaddleScience 内置的 STL 几何类 ppsci.geometry.Mesh 即可读取、解析几何文件,得到计算域,并获取几何结构边界:

+
42
+43
+44
+45
+46
+47
+48
# set geometry
+control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+# geometry bool operation
+geo = control_arm
+geom = {"geo": geo}
+# set bounds
+BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+

3.2.4 超参数设定

+

接下来需要在配置文件中指定训练轮数,此处按实验经验,使用 100 轮训练轮数,每轮进行 100 步优化。

+
# training settings
+TRAIN:
+
+

3.2.5 优化器构建

+

由于 disp_netstress_net 模型的作用仅为提供三个方向的应变 \((u, v, w)\) 和应力 \((\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz})\) 的值,并不需要进行训练,因此在构建优化器时需要注意不要使用 3.2.1 模型构建 中封装的 ModelList 作为参数,而是使用 inverse_lambda_netinverse_mu_net 组成的元组作为参数。

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并配合使用机器学习中常用的 ExponentialDecay 学习率调整策略。

+
29
+30
+31
+32
+33
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)((inverse_lambda_net, inverse_mu_net))
+
+

3.2.6 约束构建

+

本问题共涉及到 1 个约束,为结构内部点的约束 InteriorConstraint

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
# set dataloader config
+interior_constraint = ppsci.constraint.InteriorConstraint(
+    equation["LinearElasticity"].equations,
+    {
+        "stress_disp_xx": 0,
+        "stress_disp_yy": 0,
+        "stress_disp_zz": 0,
+        "stress_disp_xy": 0,
+        "stress_disp_xz": 0,
+        "stress_disp_yz": 0,
+    },
+    geom["geo"],
+    {
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "num_workers": 1,
+        "batch_size": cfg.TRAIN.batch_size.arm_interior,
+    },
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda x, y, z: (
+        (BOUNDS_X[0] < x)
+        & (x < BOUNDS_X[1])
+        & (BOUNDS_Y[0] < y)
+        & (y < BOUNDS_Y[1])
+        & (BOUNDS_Z[0] < z)
+        & (z < BOUNDS_Z[1])
+    ),
+    name="INTERIOR",
+)
+
+

InteriorConstraint 的第一个参数是方程(组)表达式,用于描述如何计算约束目标,此处填入在 3.2.2 方程构建 章节中实例化好的 equation["LinearElasticity"].equations

+

第二个参数是约束变量的目标值,在本问题中希望与 LinearElasticity 方程相关且饱含参数 \(\lambda\)\(\mu\) 的 6 个值 stress_disp_xx, stress_disp_yy, stress_disp_zz, stress_disp_xy, stress_disp_xz, stress_disp_yz 均被优化至 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.2.3 计算域构建 章节实例化好的 geom["geo"] 即可;

+

第四个参数是在计算域上的采样配置,此处设置 batch_size 为:

+
by_epoch: false
+
+

第五个参数是损失函数,此处选用常用的 MSE 函数,且 reduction 设置为 "sum",即会将参与计算的所有数据点产生的损失项求和;

+

第六个参数是几何点筛选,需要对 geo 上采样出的点进行筛选,此处传入一个 lambda 筛选函数即可,其接受点集构成的张量 x, y, z,返回布尔值张量,表示每个点是否符合筛选条件,不符合为 False,符合为 True

+

第七个参数是约束条件的名字,需要给每一个约束条件命名,方便后续对其索引。此处命名为 "INTERIOR" 即可。

+

约束构建完毕之后,以刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
constraint = {interior_constraint.name: interior_constraint}
+
+

3.2.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,由于我们使用正问题的预训练模型提供数据,因此已知 label 的值约为 \(\lambda=0.57692\)\(\mu=0.38462\)。将其包装成字典传递给 ppsci.validate.GeometryValidator 构造评估器并封装。

+
# set validator
+LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.5769
+MU = cfg.E / (2 * (1 + cfg.NU))  # 0.3846
+geom_validator = ppsci.validate.GeometryValidator(
+    {
+        "lambda_": lambda out: out["lambda_"],
+        "mu": lambda out: out["mu"],
+    },
+    {
+        "lambda_": LAMBDA_,
+        "mu": MU,
+    },
+    geom["geo"],
+    {
+        "dataset": "NamedArrayDataset",
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "total_size": cfg.EVAL.total_size.validator,
+        "batch_size": cfg.EVAL.batch_size.validator,
+    },
+    ppsci.loss.MSELoss("sum"),
+    metric={"L2Rel": ppsci.metric.L2Rel()},
+    name="geo_eval",
+)
+validator = {geom_validator.name: geom_validator}
+
+

3.2.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,可以选择合适的可视化器来对输出结果进行可视化。

+

可视化器的输入数据通过调用 PaddleScience 的 API sample_interior 产生,输出数据是\(\lambda\)\(\mu\)预测的物理量,通过设置 ppsci.visualize.VisualizerVtu ,可以将评估的输出数据保存成 vtu格式 文件,最后用可视化软件打开查看即可。

+
# set visualizer(optional)
+# add inferencer data
+samples = geom["geo"].sample_interior(
+    cfg.TRAIN.batch_size.visualizer_vtu,
+    criteria=lambda x, y, z: (
+        (BOUNDS_X[0] < x)
+        & (x < BOUNDS_X[1])
+        & (BOUNDS_Y[0] < y)
+        & (y < BOUNDS_Y[1])
+        & (BOUNDS_Z[0] < z)
+        & (z < BOUNDS_Z[1])
+    ),
+)
+pred_input_dict = {
+    k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+}
+visualizer = {
+    "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
+        pred_input_dict,
+        {
+            "lambda": lambda out: out["lambda_"],
+            "mu": lambda out: out["mu"],
+        },
+        prefix="vis",
+    )
+}
+
+

3.2.9 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    seed=cfg.seed,
+    equation=equation,
+    geom=geom,
+    save_freq=cfg.TRAIN.save_freq,
+    log_freq=cfg.log_freq,
+    eval_freq=cfg.TRAIN.eval_freq,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+    validator=validator,
+    visualizer=visualizer,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+)
+
+# train model
+solver.train()
+
+

训练后调用 ppsci.solver.Solver.plot_loss_history 可以将训练中的 loss 画出:

+
# plot losses
+solver.plot_loss_history(by_epoch=False, smooth_step=1, use_semilogy=True)
+
+

3.2.10 模型评估与可视化

+

训练完成或下载预训练模型后,通过本文档起始处“模型评估命令”进行模型评估和可视化。

+

评估和可视化过程不需要进行优化器等构建,仅需构建模型、计算域、评估器(本案例不包括)、可视化器,然后按顺序传递给 ppsci.solver.Solver 启动评估和可视化。

+
def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
+    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
+    # wrap to a model_list
+    model = ppsci.arch.ModelList(
+        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
+    )
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set validator
+    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.57692
+    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.38462
+    geom_validator = ppsci.validate.GeometryValidator(
+        {
+            "lambda_": lambda out: out["lambda_"],
+            "mu": lambda out: out["mu"],
+        },
+        {
+            "lambda_": LAMBDA_,
+            "mu": MU,
+        },
+        geom["geo"],
+        {
+            "dataset": "NamedArrayDataset",
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "total_size": cfg.EVAL.total_size.validator,
+            "batch_size": cfg.EVAL.batch_size.validator,
+        },
+        ppsci.loss.MSELoss("sum"),
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="geo_eval",
+    )
+    validator = {geom_validator.name: geom_validator}
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.EVAL.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "lambda": lambda out: out["lambda_"],
+                "mu": lambda out: out["mu"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        log_freq=cfg.log_freq,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+

4. 完整代码

+
forward_analysis.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
from os import path as osp
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+from paddle import distributed as dist
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+    # set parallel
+    enable_parallel = dist.get_world_size() > 1
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    # wrap to a model_list
+    model_list = ppsci.arch.ModelList((disp_net, stress_net))
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+        **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model_list)
+
+    # specify parameters
+    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))
+    MU = cfg.E / (2 * (1 + cfg.NU))
+
+    # set equation
+    equation = {
+        "LinearElasticity": ppsci.equation.LinearElasticity(
+            E=None, nu=None, lambda_=LAMBDA_, mu=MU, dim=3
+        )
+    }
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    geom = {"geo": control_arm}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "num_workers": 1,
+    }
+
+    # set constraint
+    arm_left_constraint = ppsci.constraint.BoundaryConstraint(
+        equation["LinearElasticity"].equations,
+        {"traction_x": cfg.T[0], "traction_y": cfg.T[1], "traction_z": cfg.T[2]},
+        geom["geo"],
+        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_left},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda x, y, z: np.sqrt(
+            np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
+            + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
+        )
+        <= cfg.CIRCLE_LEFT_RADIUS + 1e-1,
+        name="BC_LEFT",
+    )
+    arm_right_constraint = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda d: d["u"], "v": lambda d: d["v"], "w": lambda d: d["w"]},
+        {"u": 0, "v": 0, "w": 0},
+        geom["geo"],
+        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_right},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda x, y, z: np.sqrt(
+            np.square(x - cfg.CIRCLE_RIGHT_CENTER_XZ[0])
+            + np.square(z - cfg.CIRCLE_RIGHT_CENTER_XZ[1])
+        )
+        <= cfg.CIRCLE_RIGHT_RADIUS + 1e-1,
+        weight_dict=cfg.TRAIN.weight.arm_right,
+        name="BC_RIGHT",
+    )
+    arm_surface_constraint = ppsci.constraint.BoundaryConstraint(
+        equation["LinearElasticity"].equations,
+        {"traction_x": 0, "traction_y": 0, "traction_z": 0},
+        geom["geo"],
+        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_surface},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda x, y, z: np.sqrt(
+            np.square(x - cfg.CIRCLE_LEFT_CENTER_XY[0])
+            + np.square(y - cfg.CIRCLE_LEFT_CENTER_XY[1])
+        )
+        > cfg.CIRCLE_LEFT_RADIUS + 1e-1,
+        name="BC_SURFACE",
+    )
+    arm_interior_constraint = ppsci.constraint.InteriorConstraint(
+        equation["LinearElasticity"].equations,
+        {
+            "equilibrium_x": 0,
+            "equilibrium_y": 0,
+            "equilibrium_z": 0,
+            "stress_disp_xx": 0,
+            "stress_disp_yy": 0,
+            "stress_disp_zz": 0,
+            "stress_disp_xy": 0,
+            "stress_disp_xz": 0,
+            "stress_disp_yz": 0,
+        },
+        geom["geo"],
+        {**train_dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.arm_interior},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+        weight_dict={
+            "equilibrium_x": "sdf",
+            "equilibrium_y": "sdf",
+            "equilibrium_z": "sdf",
+            "stress_disp_xx": "sdf",
+            "stress_disp_yy": "sdf",
+            "stress_disp_zz": "sdf",
+            "stress_disp_xy": "sdf",
+            "stress_disp_xz": "sdf",
+            "stress_disp_yz": "sdf",
+        },
+        name="INTERIOR",
+    )
+
+    # re-assign to cfg.TRAIN.iters_per_epoch
+    if enable_parallel:
+        cfg.TRAIN.iters_per_epoch = len(arm_left_constraint.data_loader)
+
+    # wrap constraints togetherg
+    constraint = {
+        arm_left_constraint.name: arm_left_constraint,
+        arm_right_constraint.name: arm_right_constraint,
+        arm_surface_constraint.name: arm_surface_constraint,
+        arm_interior_constraint.name: arm_interior_constraint,
+    }
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.TRAIN.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "u": lambda out: out["u"],
+                "v": lambda out: out["v"],
+                "w": lambda out: out["w"],
+                "sigma_xx": lambda out: out["sigma_xx"],
+                "sigma_yy": lambda out: out["sigma_yy"],
+                "sigma_zz": lambda out: out["sigma_zz"],
+                "sigma_xy": lambda out: out["sigma_xy"],
+                "sigma_xz": lambda out: out["sigma_xz"],
+                "sigma_yz": lambda out: out["sigma_yz"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        eval_freq=cfg.TRAIN.eval_freq,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+        visualizer=visualizer,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+
+    # train model
+    solver.train()
+
+    # plot losses
+    solver.plot_loss_history(by_epoch=True, smooth_step=1)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    # wrap to a model_list
+    model_list = ppsci.arch.ModelList((disp_net, stress_net))
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.TRAIN.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_u_v_w_sigmas": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "u": lambda out: out["u"],
+                "v": lambda out: out["v"],
+                "w": lambda out: out["w"],
+                "sigma_xx": lambda out: out["sigma_xx"],
+                "sigma_yy": lambda out: out["sigma_yy"],
+                "sigma_zz": lambda out: out["sigma_zz"],
+                "sigma_xy": lambda out: out["sigma_xy"],
+                "sigma_xz": lambda out: out["sigma_xz"],
+                "sigma_yz": lambda out: out["sigma_yz"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        geom=geom,
+        log_freq=cfg.log_freq,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    # wrap to a model_list
+    model_list = ppsci.arch.ModelList((disp_net, stress_net))
+
+    # load pretrained model
+    solver = ppsci.solver.Solver(
+        model=model_list, pretrained_model_path=cfg.INFER.pretrained_model_path
+    )
+
+    # export models
+    input_spec = [
+        {
+            key: InputSpec([None, 1], "float32", name=key)
+            for key in cfg.MODEL.disp_net.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+    from ppsci.visualize import vtu
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.TRAIN.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+
+    output_dict = predictor.predict(pred_input_dict, cfg.INFER.batch_size)
+
+    # mapping data to output_keys
+    output_keys = cfg.MODEL.disp_net.output_keys + cfg.MODEL.stress_net.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+    output_dict.update(pred_input_dict)
+
+    vtu.save_vtu_from_dict(
+        osp.join(cfg.output_dir, "vis"),
+        output_dict,
+        cfg.MODEL.disp_net.input_keys,
+        output_keys,
+        1,
+    )
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="forward_analysis.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
inverse_parameter.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
from os import path as osp
+
+import hydra
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
+    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
+    # freeze models
+    disp_net.freeze()
+    stress_net.freeze()
+    # wrap to a model_list
+    model = ppsci.arch.ModelList(
+        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
+    )
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+        **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)((inverse_lambda_net, inverse_mu_net))
+
+    # set equation
+    equation = {
+        "LinearElasticity": ppsci.equation.LinearElasticity(
+            E=None, nu=None, lambda_="lambda_", mu="mu", dim=3
+        )
+    }
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set dataloader config
+    interior_constraint = ppsci.constraint.InteriorConstraint(
+        equation["LinearElasticity"].equations,
+        {
+            "stress_disp_xx": 0,
+            "stress_disp_yy": 0,
+            "stress_disp_zz": 0,
+            "stress_disp_xy": 0,
+            "stress_disp_xz": 0,
+            "stress_disp_yz": 0,
+        },
+        geom["geo"],
+        {
+            "dataset": "NamedArrayDataset",
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": True,
+                "shuffle": True,
+            },
+            "num_workers": 1,
+            "batch_size": cfg.TRAIN.batch_size.arm_interior,
+        },
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+        name="INTERIOR",
+    )
+    constraint = {interior_constraint.name: interior_constraint}
+
+    # set validator
+    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.5769
+    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.3846
+    geom_validator = ppsci.validate.GeometryValidator(
+        {
+            "lambda_": lambda out: out["lambda_"],
+            "mu": lambda out: out["mu"],
+        },
+        {
+            "lambda_": LAMBDA_,
+            "mu": MU,
+        },
+        geom["geo"],
+        {
+            "dataset": "NamedArrayDataset",
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "total_size": cfg.EVAL.total_size.validator,
+            "batch_size": cfg.EVAL.batch_size.validator,
+        },
+        ppsci.loss.MSELoss("sum"),
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="geo_eval",
+    )
+    validator = {geom_validator.name: geom_validator}
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.TRAIN.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "lambda": lambda out: out["lambda_"],
+                "mu": lambda out: out["mu"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        eval_freq=cfg.TRAIN.eval_freq,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    )
+
+    # train model
+    solver.train()
+
+    # plot losses
+    solver.plot_loss_history(by_epoch=False, smooth_step=1, use_semilogy=True)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
+    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
+    # wrap to a model_list
+    model = ppsci.arch.ModelList(
+        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
+    )
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+
+    # set validator
+    LAMBDA_ = cfg.NU * cfg.E / ((1 + cfg.NU) * (1 - 2 * cfg.NU))  # 0.57692
+    MU = cfg.E / (2 * (1 + cfg.NU))  # 0.38462
+    geom_validator = ppsci.validate.GeometryValidator(
+        {
+            "lambda_": lambda out: out["lambda_"],
+            "mu": lambda out: out["mu"],
+        },
+        {
+            "lambda_": LAMBDA_,
+            "mu": MU,
+        },
+        geom["geo"],
+        {
+            "dataset": "NamedArrayDataset",
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "total_size": cfg.EVAL.total_size.validator,
+            "batch_size": cfg.EVAL.batch_size.validator,
+        },
+        ppsci.loss.MSELoss("sum"),
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="geo_eval",
+    )
+    validator = {geom_validator.name: geom_validator}
+
+    # set visualizer(optional)
+    # add inferencer data
+    samples = geom["geo"].sample_interior(
+        cfg.EVAL.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+    visualizer = {
+        "visulzie_lambda_mu": ppsci.visualize.VisualizerVtu(
+            pred_input_dict,
+            {
+                "lambda": lambda out: out["lambda_"],
+                "mu": lambda out: out["mu"],
+            },
+            prefix="vis",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        log_freq=cfg.log_freq,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set model
+    disp_net = ppsci.arch.MLP(**cfg.MODEL.disp_net)
+    stress_net = ppsci.arch.MLP(**cfg.MODEL.stress_net)
+    inverse_lambda_net = ppsci.arch.MLP(**cfg.MODEL.inverse_lambda_net)
+    inverse_mu_net = ppsci.arch.MLP(**cfg.MODEL.inverse_mu_net)
+    # wrap to a model_list
+    model = ppsci.arch.ModelList(
+        (disp_net, stress_net, inverse_lambda_net, inverse_mu_net)
+    )
+
+    # load pretrained model
+    solver = ppsci.solver.Solver(
+        model=model, pretrained_model_path=cfg.INFER.pretrained_model_path
+    )
+
+    # export models
+    input_spec = [
+        {
+            key: InputSpec([None, 1], "float32", name=key)
+            for key in cfg.MODEL.disp_net.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+    from ppsci.visualize import vtu
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    control_arm = ppsci.geometry.Mesh(cfg.GEOM_PATH)
+    # geometry bool operation
+    geo = control_arm
+    geom = {"geo": geo}
+    # set bounds
+    BOUNDS_X, BOUNDS_Y, BOUNDS_Z = control_arm.bounds
+    samples = geom["geo"].sample_interior(
+        cfg.EVAL.batch_size.visualizer_vtu,
+        criteria=lambda x, y, z: (
+            (BOUNDS_X[0] < x)
+            & (x < BOUNDS_X[1])
+            & (BOUNDS_Y[0] < y)
+            & (y < BOUNDS_Y[1])
+            & (BOUNDS_Z[0] < z)
+            & (z < BOUNDS_Z[1])
+        ),
+    )
+    pred_input_dict = {
+        k: v for k, v in samples.items() if k in cfg.MODEL.disp_net.input_keys
+    }
+
+    output_dict = predictor.predict(pred_input_dict, cfg.INFER.batch_size)
+
+    # mapping data to output_keys
+    output_keys = (
+        cfg.MODEL.disp_net.output_keys
+        + cfg.MODEL.stress_net.output_keys
+        + cfg.MODEL.inverse_lambda_net.output_keys
+        + cfg.MODEL.inverse_mu_net.output_keys
+    )
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+    output_dict.update(pred_input_dict)
+    vtu.save_vtu_from_dict(
+        osp.join(cfg.output_dir, "vis"),
+        output_dict,
+        cfg.MODEL.disp_net.input_keys,
+        output_keys,
+        1,
+    )
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="inverse_parameter.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

5.1 受力分析求解

+

下面展示了当力的方向为 x 正方向时 3 个方向的应变 \(u, v, w\) 以及 6 个应力 \(\sigma_{xx}, \sigma_{yy}, \sigma_{zz}, \sigma_{xy}, \sigma_{xz}, \sigma_{yz}\) 的模型预测结果,结果基本符合认知。

+
+

forward_result.jpg +

+
左侧为预测的结构应变 u;中间为预测的结构应变 v;右侧为预测的结构应变 w
+
+
+

forward_result.jpg +

+
左侧为预测的结构应力 sigma_xx;中间为预测的结构应力 sigma_xy;右侧为预测的结构应力 sigma_xz
+
+
+

forward_result.jpg +

+
左侧为预测的结构应力 sigma_yy;中间为预测的结构应力 sigma_yz;右侧为预测的结构应力 sigma_zz
+
+

5.2 参数逆推求解

+

下面展示了线弹性方程参数 \(\lambda, \mu\) 的模型预测结果,在结构的大部分区域预测误差在 1% 左右。

+ + + + + + + + + + + + + + + + + + + + +
datalambdamu
outs(mean)0.549500.38642
label0.576920.38462
+
+

forward_result.jpg +

+
左侧为预测的 lambda;右侧为预测的 mu
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/cylinder2d_unsteady/index.html b/zh/examples/cylinder2d_unsteady/index.html new file mode 100644 index 0000000000..e07e4c9b00 --- /dev/null +++ b/zh/examples/cylinder2d_unsteady/index.html @@ -0,0 +1,5056 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cylinder2D_unsteady - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-Cylinder(2D Flow Around a Cylinder)

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar -o cylinder2d_unsteady_Re100_dataset.tar
+# unzip it
+tar -xvf cylinder2d_unsteady_Re100_dataset.tar
+python cylinder2d_unsteady_Re100.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar -o cylinder2d_unsteady_Re100_dataset.tar
+# unzip it
+tar -xvf cylinder2d_unsteady_Re100_dataset.tar
+python cylinder2d_unsteady_Re100.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_pretrained.pdparams
+
+
+
+
python cylinder2d_unsteady_Re100.py mode=export
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar -o cylinder2d_unsteady_Re100_dataset.tar
+# unzip it
+tar -xvf cylinder2d_unsteady_Re100_dataset.tar
+python cylinder2d_unsteady_Re100.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
cylinder2d_unsteady_Re100_pretrained.pdparamsloss(Residual): 0.00398
MSE.continuity(Residual): 0.00126
MSE.momentum_x(Residual): 0.00151
MSE.momentum_y(Residual): 0.00120
+

1. 背景简介

+

圆柱绕流问题可以应用于很多领域。例如,在工业设计中,它可以被用来模拟和优化流体在各种设备中的流动,如风力发电机、汽车和飞机的流体动力学性能等。在环保领域,圆柱绕流问题也有应用,如预测和控制河流的洪水、研究污染物的扩散等。此外,在工程实践中,如流体动力学、流体静力学、热交换、空气动力学等领域,圆柱绕流问题也具有实际意义。

+

2D Flow Around a Cylinder,中文名称可译作“2维圆柱绕流”,是指二维圆柱低速定常绕流的流型只与 \(Re\) 数有关。在 \(Re \le 1\) 时,流场中的惯性力与粘性力相比居次要地位,圆柱上下游的流线前后对称,阻力系数近似与 \(Re\) 成反比(阻力系数为 10~60),此 \(Re\) 数范围的绕流称为斯托克斯区;随着 \(Re\) 的增大,圆柱上下游的流线逐渐失去对称性。

+

2. 问题定义

+

质量守恒:

+
\[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\dfrac{\partial u}{\partial t} + u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} + \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\dfrac{\partial v}{\partial t} + u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} + \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) +\]
+

令:

+

\(t^* = \dfrac{L}{U_0}\)

+

\(x^*=y^* = L\)

+

\(u^*=v^* = U_0\)

+

\(p^* = \rho {U_0}^2\)

+

定义:

+

无量纲时间 \(\tau = \dfrac{t}{t^*}\)

+

无量纲坐标 \(x:X = \dfrac{x}{x^*}\);无量纲坐标 \(y:Y = \dfrac{y}{y^*}\)

+

无量纲速度 \(x:U = \dfrac{u}{u^*}\);无量纲速度 \(y:V = \dfrac{v}{u^*}\)

+

无量纲压力 \(P = \dfrac{p}{p^*}\)

+

雷诺数 \(Re = \dfrac{L U_0}{\nu}\)

+

则可获得如下无量纲Navier-Stokes方程,施加于流体域内部:

+

质量守恒:

+
\[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\dfrac{\partial U}{\partial \tau} + U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} = -\dfrac{\partial P}{\partial X} + \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\dfrac{\partial V}{\partial \tau} + U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} = -\dfrac{\partial P}{\partial Y} + \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) +\]
+

对于流体域边界和流体域内部圆周边界,则需施加 Dirichlet 边界条件:

+

流体域入口边界:

+
\[ +u=1, v=0 +\]
+

圆周边界:

+
\[ +u=0, v=0 +\]
+

流体域出口边界:

+
\[ +p=0 +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

在开始构建代码之前,请先按照下列命令下载训练、评估所需的数据集

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/cylinder2d_unsteady_Re100/cylinder2d_unsteady_Re100_dataset.tar
+tar -xf cylinder2d_unsteady_Re100_dataset.tar
+
+

3.1 模型构建

+

在 2D-Cylinder 问题中,每一个已知的坐标点 \((t, x, y)\) 都有自身的横向速度 \(u\)、纵向速度 \(v\)、压力 \(p\) +三个待求解的未知量,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((t, x, y)\)\((u, v, p)\) 的映射函数 \(f: \mathbb{R}^3 \to \mathbb{R}^3\) ,即:

+
\[ +u, v, p = f(t, x, y) +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ["t", "x", "y"],输出变量名是 ["u", "v", "p"],这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 9 层隐藏神经元,每层神经元数为 50,使用 "tanh" 作为激活函数的神经网络模型 model

+

3.2 方程构建

+

由于 2D-Cylinder 使用的是 Navier-Stokes 方程的2维瞬态形式,因此可以直接使用 PaddleScience 内置的 NavierStokes

+
36
+37
+38
+39
# set equation
+equation = {
+    "NavierStokes": ppsci.equation.NavierStokes(cfg.VISCOSITY, cfg.DENSITY, 2, True)
+}
+
+

在实例化 NavierStokes 类时需指定必要的参数:动力粘度 \(\nu=0.02\), 流体密度 \(\rho=1.0\)

+

3.3 计算域构建

+

本文中 2D-Cylinder 的计算域由 CSV 文件储存的点云构成,因此可以直接使用 PaddleScience 内置的点云几何 PointCloud 和时间域 TimeDomain,组合成时间-空间的 TimeXGeometry 计算域。

+
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
# set timestamps
+train_timestamps = np.linspace(
+    cfg.TIME_START, cfg.TIME_END, cfg.NUM_TIMESTAMPS, endpoint=True
+).astype("float32")
+train_timestamps = np.random.choice(train_timestamps, cfg.TRAIN_NUM_TIMESTAMPS)
+train_timestamps.sort()
+t0 = np.array([cfg.TIME_START], dtype="float32")
+
+val_timestamps = np.linspace(
+    cfg.TIME_START, cfg.TIME_END, cfg.NUM_TIMESTAMPS, endpoint=True
+).astype("float32")
+
+logger.message(f"train_timestamps: {train_timestamps.tolist()}")
+logger.message(f"val_timestamps: {val_timestamps.tolist()}")
+
+# set time-geometry
+geom = {
+    "time_rect": ppsci.geometry.TimeXGeometry(
+        ppsci.geometry.TimeDomain(
+            cfg.TIME_START,
+            cfg.TIME_END,
+            timestamps=np.concatenate((t0, train_timestamps), axis=0),
+        ),
+        ppsci.geometry.PointCloud(
+            reader.load_csv_file(
+                "./datasets/domain_train.csv",
+                ("x", "y"),
+                alias_dict={"x": "Points:0", "y": "Points:1"},
+            ),
+            ("x", "y"),
+        ),
+    ),
+    "time_rect_eval": ppsci.geometry.PointCloud(
+        reader.load_csv_file(
+            "./datasets/domain_eval.csv",
+            ("t", "x", "y"),
+        ),
+        ("t", "x", "y"),
+    ),
+}
+
+
    +
  1. 评估数据点已包含时间戳信息,因此无需额外再与 TimeDomain 组合成 TimeXGeometry,只需使用 PointCloud 读入数据即可。
  2. +
+
+提示 +

PointCloudTimeDomain 是两种可以单独使用的 Geometry 派生类。

+

如输入数据只来自于点云几何,则可以直接使用 ppsci.geometry.PointCloud(...) 创建空间几何域对象;

+

如输入数据只来自一维时间域,则可以直接使用 ppsci.geometry.TimeDomain(...) 构建时间域对象。

+
+

3.4 约束构建

+

根据 2. 问题定义 得到的无量纲公式和和边界条件,对应了在计算域中指导模型训练的三个约束条件,即:

+
    +
  1. +

    施加在流体域内部点上的无量纲 Navier-Stokes 方程约束(经过简单移项)

    +
    \[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
    +
    \[ +\dfrac{\partial U}{\partial \tau} + U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} + \dfrac{\partial P}{\partial X} - \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) = 0 +\]
    +
    \[ +\dfrac{\partial V}{\partial \tau} + U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} + \dfrac{\partial P}{\partial Y} - \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) = 0 +\]
    +

    为了方便获取中间变量,NavierStokes 类内部将上式左侧的结果分别命名为 continuity, momentum_x, momentum_y

    +
  2. +
  3. +

    施加在流体域入口、内部圆周、流体域出口的 Dirichlet 边界条件约束

    +

    流体域入口边界:

    +
    \[ +u=1, v=0 +\]
    +

    流体域出口边界:

    +
    \[ +p=0 +\]
    +

    圆周边界:

    +
    \[ +u=0, v=0 +\]
    +
  4. +
  5. +

    施加在初始时刻流体域内部点上的初值条件约束:

    +
    \[ +u=u_{t0}, v=v_{t0}, p=p_{t0} +\]
    +
  6. +
+

接下来使用 PaddleScience 内置的 InteriorConstraintSupervisedConstraint 构建上述两种约束条件。

+

在定义约束之前,需要给每一种约束指定采样点个数,表示每一种约束在其对应计算域内采样数据的数量,以及通用的采样配置。

+
82
+83
+84
# pde/bc/sup constraint use t1~tn, initial constraint use t0
+NTIME_PDE = len(train_timestamps)
+ALIAS_DICT = {"x": "Points:0", "y": "Points:1", "u": "U:0", "v": "U:1"}
+
+

3.4.1 内部点约束

+

以作用在流体域内部点上的 InteriorConstraint 为例,代码如下:

+
86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
# set constraint
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["NavierStokes"].equations,
+    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+    geom["time_rect"],
+    {
+        "dataset": "IterableNamedArrayDataset",
+        "batch_size": cfg.NPOINT_PDE * NTIME_PDE,
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="EQ",
+)
+
+

InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["NavierStokes"].equations

+

第二个参数是约束变量的目标值,在本问题中我们希望 Navier-Stokes 方程产生的三个中间结果 continuity, momentum_x, momentum_y 被优化至 0,因此将它们的目标值全部设为 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["time_rect"] 即可;

+

第四个参数是在计算域上的采样配置,此处我们使用全量数据点训练,因此 dataset 字段设置为 "IterableNamedArrayDataset" 且 iters_per_epoch 也设置为 1,采样点数 batch_size 设为 9420 * 30(表示一个时刻产生 9420 个数据点,共有 30 个时刻);

+

第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第六个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

+

3.4.2 边界约束

+

同理,我们还需要构建流体域的流入边界、流出边界、圆周边界共三个边界的 Dirichlet 边界约束。以 bc_inlet_cylinder 边界约束为例,由于作用区域是边界且边界上的数据由 CSV 文件记录,因此我们使用 SupervisedConstraint 类,并按照如下规则指定第一个参数 dataloader_cfg 配置字典:

+
    +
  • +

    该配置字典的第一个参数为包含 CSV 文件的路径 ./datasets/domain_inlet_cylinder.csv 在内的配置字典;

    +
  • +
  • +

    该配置字典的第一个参数指定数据加载方式,此处我们使用 IterableCSVDataset 作为全量数据加载器;

    +
  • +
  • +

    该配置字典的第二个参数指定数据的加载路径,此处填写 ./datasets/domain_inlet_cylinder.csv

    +
  • +
  • +

    该配置字典的第三个参数指定要从文件中读取的输入列,对应转换后关键字,此处填写为 ("x", "y")

    +
  • +
  • +

    该配置字典的第四个参数指定要从文件中读取的标签列,对应转换后关键字,此处填写为 ("u", "v")

    +
  • +
  • +

    考虑到同一个变量在不同 CSV 文件中可能具有不同的字段名,而且有的字段名过长在编写代码时容易写错,因此该配置字典的第五个参数用于指定字段列的别名,此处填写为 {"x": "Points:0", "y": "Points:1", "u": "U:0", "v": "U:1"}

    +
  • +
  • +

    该配置字典的第六个参数指定每个标签在计算损失时的权重,此处我们放大 "u" 和 "v" 的权重至 10,填写 {"u": 10, "v": 10}

    +
  • +
  • +

    该配置字典的第七个参数指定数据读取是否涉及时间信息,此处我们设定为训练时间戳,即填写 train_timestamps

    +
  • +
+

第二个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第三个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "BC_inlet_cylinder" 即可。

+

剩下的 bc_outlet 按照相同原理构建,代码如下所示:

+
bc_inlet_cylinder = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableCSVDataset",
+            "file_path": cfg.DOMAIN_INLET_CYLINDER_PATH,
+            "input_keys": ("x", "y"),
+            "label_keys": ("u", "v"),
+            "alias_dict": ALIAS_DICT,
+            "weight_dict": {"u": 10, "v": 10},
+            "timestamps": train_timestamps,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="BC_inlet_cylinder",
+)
+bc_outlet = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableCSVDataset",
+            "file_path": cfg.DOMAIN_OUTLET_PATH,
+            "input_keys": ("x", "y"),
+            "label_keys": ("p",),
+            "alias_dict": ALIAS_DICT,
+            "timestamps": train_timestamps,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="BC_outlet",
+)
+
+

3.4.3 初值约束

+

对于 \(t=t_0\) 时刻的流体域内的点,我们还需要对 \(u\), \(v\), \(p\) 施加初值约束,代码如下:

+
ic = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableCSVDataset",
+            "file_path": cfg.IC0_1_PATH,
+            "input_keys": ("x", "y"),
+            "label_keys": ("u", "v", "p"),
+            "alias_dict": ALIAS_DICT,
+            "weight_dict": {"u": 10, "v": 10, "p": 10},
+            "timestamps": t0,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="IC",
+)
+
+

3.4.4 监督约束

+

本案例在流体域内部加入了一定数量的监督点来保证模型最终的收敛情况,因此最后还需要加入一个监督约束,数据同样来自 CSV 文件,代码如下:

+
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableCSVDataset",
+            "file_path": cfg.PROBE1_50_PATH,
+            "input_keys": ("t", "x", "y"),
+            "label_keys": ("u", "v"),
+            "alias_dict": ALIAS_DICT,
+            "weight_dict": {"u": 10, "v": 10},
+            "timestamps": train_timestamps,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="Sup",
+)
+
+

在微分方程约束、边界约束、初值约束、监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    pde_constraint.name: pde_constraint,
+    bc_inlet_cylinder.name: bc_inlet_cylinder,
+    bc_outlet.name: bc_outlet,
+    ic.name: ic,
+    sup_constraint.name: sup_constraint,
+}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用四万轮训练轮数,评估间隔为四百轮,学习率设为 0.001。

+

+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
# set validator
+NPOINT_EVAL = (
+    cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET
+) * cfg.NUM_TIMESTAMPS
+residual_validator = ppsci.validate.GeometryValidator(
+    equation["NavierStokes"].equations,
+    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+    geom["time_rect_eval"],
+    {
+        "dataset": "NamedArrayDataset",
+        "total_size": NPOINT_EVAL,
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {"name": "BatchSampler"},
+    },
+    ppsci.loss.MSELoss("mean"),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="Residual",
+)
+validator = {residual_validator.name: residual_validator}
+
+

方程设置与 约束构建 的设置相同,表示如何计算所需评估的目标变量;

+

此处我们为 momentum_x, continuity, momentum_y 三个目标变量设置标签值为 0;

+

计算域与 约束构建 的设置相同,表示在指定计算域上进行评估;

+

采样点配置则需要指定总的评估点数 total_size,此处我们设置为 9662 * 50(9420个流体域内的点+161个流体域流入边界点+81个流体域流出边界点,共 50 个评估时刻);

+

评价指标 metric 选择 ppsci.metric.MSE 即可;

+

其余配置与 约束构建 的设置类似。

+

3.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文中的输出数据是一个区域内的二维点集,每个时刻 \(t\) 的坐标是 \((x^t_i, y^t_i)\),对应值是 \((u^t_i, v^t_i, p^t_i)\),因此我们只需要将评估的输出数据按时刻保存成 50 个 vtu格式 文件,最后用可视化软件打开查看即可。代码如下:

+
# set visualizer(optional)
+vis_points = geom["time_rect_eval"].sample_interior(
+    (cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET)
+    * cfg.NUM_TIMESTAMPS,
+    evenly=True,
+)
+visualizer = {
+    "visualize_u_v_p": ppsci.visualize.VisualizerVtu(
+        vis_points,
+        {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+        num_timestamps=cfg.NUM_TIMESTAMPS,
+        prefix="result_u_v_p",
+    )
+}
+
+

3.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
cylinder2d_unsteady_Re100.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+from ppsci.utils import reader
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(cfg.VISCOSITY, cfg.DENSITY, 2, True)
+    }
+
+    # set timestamps
+    train_timestamps = np.linspace(
+        cfg.TIME_START, cfg.TIME_END, cfg.NUM_TIMESTAMPS, endpoint=True
+    ).astype("float32")
+    train_timestamps = np.random.choice(train_timestamps, cfg.TRAIN_NUM_TIMESTAMPS)
+    train_timestamps.sort()
+    t0 = np.array([cfg.TIME_START], dtype="float32")
+
+    val_timestamps = np.linspace(
+        cfg.TIME_START, cfg.TIME_END, cfg.NUM_TIMESTAMPS, endpoint=True
+    ).astype("float32")
+
+    logger.message(f"train_timestamps: {train_timestamps.tolist()}")
+    logger.message(f"val_timestamps: {val_timestamps.tolist()}")
+
+    # set time-geometry
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(
+                cfg.TIME_START,
+                cfg.TIME_END,
+                timestamps=np.concatenate((t0, train_timestamps), axis=0),
+            ),
+            ppsci.geometry.PointCloud(
+                reader.load_csv_file(
+                    cfg.DOMAIN_TRAIN_PATH,
+                    ("x", "y"),
+                    alias_dict={"x": "Points:0", "y": "Points:1"},
+                ),
+                ("x", "y"),
+            ),
+        ),
+        "time_rect_eval": ppsci.geometry.PointCloud(
+            reader.load_csv_file(
+                cfg.DOMAIN_EVAL_PATH,
+                ("t", "x", "y"),
+            ),
+            ("t", "x", "y"),
+        ),
+    }
+
+    # pde/bc/sup constraint use t1~tn, initial constraint use t0
+    NTIME_PDE = len(train_timestamps)
+    ALIAS_DICT = {"x": "Points:0", "y": "Points:1", "u": "U:0", "v": "U:1"}
+
+    # set constraint
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom["time_rect"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "batch_size": cfg.NPOINT_PDE * NTIME_PDE,
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="EQ",
+    )
+    bc_inlet_cylinder = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableCSVDataset",
+                "file_path": cfg.DOMAIN_INLET_CYLINDER_PATH,
+                "input_keys": ("x", "y"),
+                "label_keys": ("u", "v"),
+                "alias_dict": ALIAS_DICT,
+                "weight_dict": {"u": 10, "v": 10},
+                "timestamps": train_timestamps,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="BC_inlet_cylinder",
+    )
+    bc_outlet = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableCSVDataset",
+                "file_path": cfg.DOMAIN_OUTLET_PATH,
+                "input_keys": ("x", "y"),
+                "label_keys": ("p",),
+                "alias_dict": ALIAS_DICT,
+                "timestamps": train_timestamps,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="BC_outlet",
+    )
+    ic = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableCSVDataset",
+                "file_path": cfg.IC0_1_PATH,
+                "input_keys": ("x", "y"),
+                "label_keys": ("u", "v", "p"),
+                "alias_dict": ALIAS_DICT,
+                "weight_dict": {"u": 10, "v": 10, "p": 10},
+                "timestamps": t0,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="IC",
+    )
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableCSVDataset",
+                "file_path": cfg.PROBE1_50_PATH,
+                "input_keys": ("t", "x", "y"),
+                "label_keys": ("u", "v"),
+                "alias_dict": ALIAS_DICT,
+                "weight_dict": {"u": 10, "v": 10},
+                "timestamps": train_timestamps,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="Sup",
+    )
+
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        bc_inlet_cylinder.name: bc_inlet_cylinder,
+        bc_outlet.name: bc_outlet,
+        ic.name: ic,
+        sup_constraint.name: sup_constraint,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    NPOINT_EVAL = (
+        cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET
+    ) * cfg.NUM_TIMESTAMPS
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom["time_rect_eval"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": NPOINT_EVAL,
+            "batch_size": cfg.EVAL.batch_size,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("mean"),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer(optional)
+    vis_points = geom["time_rect_eval"].sample_interior(
+        (cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET)
+        * cfg.NUM_TIMESTAMPS,
+        evenly=True,
+    )
+    visualizer = {
+        "visualize_u_v_p": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+            num_timestamps=cfg.NUM_TIMESTAMPS,
+            prefix="result_u_v_p",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(cfg.VISCOSITY, cfg.DENSITY, 2, True)
+    }
+
+    # set timestamps
+    val_timestamps = np.linspace(
+        cfg.TIME_START, cfg.TIME_END, cfg.NUM_TIMESTAMPS, endpoint=True
+    ).astype("float32")
+
+    logger.message(f"val_timestamps: {val_timestamps.tolist()}")
+
+    # set time-geometry
+    geom = {
+        "time_rect_eval": ppsci.geometry.PointCloud(
+            reader.load_csv_file(
+                cfg.DOMAIN_EVAL_PATH,
+                ("t", "x", "y"),
+            ),
+            ("t", "x", "y"),
+        ),
+    }
+
+    # set validator
+    NPOINT_EVAL = (
+        cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET
+    ) * cfg.NUM_TIMESTAMPS
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom["time_rect_eval"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": NPOINT_EVAL,
+            "batch_size": cfg.EVAL.batch_size,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("mean"),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer(optional)
+    vis_points = geom["time_rect_eval"].sample_interior(
+        (cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET)
+        * cfg.NUM_TIMESTAMPS,
+        evenly=True,
+    )
+    visualizer = {
+        "visualize_u_v_p": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+            num_timestamps=cfg.NUM_TIMESTAMPS,
+            prefix="result_u_v_p",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        geom=geom,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    # evaluate
+    solver.eval()
+    # visualize prediction
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+    # set time-geometry
+    geom = {
+        "time_rect_eval": ppsci.geometry.PointCloud(
+            reader.load_csv_file(
+                cfg.DOMAIN_EVAL_PATH,
+                ("t", "x", "y"),
+            ),
+            ("t", "x", "y"),
+        ),
+    }
+    NPOINT_EVAL = (
+        cfg.NPOINT_PDE + cfg.NPOINT_INLET_CYLINDER + cfg.NPOINT_OUTLET
+    ) * cfg.NUM_TIMESTAMPS
+    input_dict = geom["time_rect_eval"].sample_interior(NPOINT_EVAL, evenly=True)
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    ppsci.visualize.save_vtu_from_dict(
+        "./cylinder2d_unsteady_Re100_pred.vtu",
+        {**input_dict, **output_dict},
+        input_dict.keys(),
+        cfg.MODEL.output_keys,
+        cfg.NUM_TIMESTAMPS,
+    )
+
+
+@hydra.main(
+    version_base=None,
+    config_path="./conf",
+    config_name="cylinder2d_unsteady_Re100.yaml",
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

预测结果如下所示,图像的横轴是水平方向,纵轴代表竖直方向,流体流向为从左到右,图片中展示的是模型预测50个时刻对应流场的横向流速\(u(t,x,y)\)的结果。

+
+说明 +

本案例只作为demo展示,尚未进行充分调优,下方部分展示结果可能与 OpenFOAM 存在一定差别。

+
+
+

u_pred.gif +

+
横向流速u的模型预测结果
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/cylinder2d_unsteady_transformer_physx/index.html b/zh/examples/cylinder2d_unsteady_transformer_physx/index.html new file mode 100644 index 0000000000..03ed09ba7f --- /dev/null +++ b/zh/examples/cylinder2d_unsteady_transformer_physx/index.html @@ -0,0 +1,5817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cylinder2D_unsteady_transform_physx - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-Cylinder(2D Flow Around a Cylinder)

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 --create-dirs -o ./datasets/cylinder_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 --create-dirs -o ./datasets/cylinder_valid.hdf5
+python train_enn.py
+python train_transformer.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 --create-dirs -o ./datasets/cylinder_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 --create-dirs -o ./datasets/cylinder_valid.hdf5
+python train_enn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder/cylinder_pretrained.pdparams
+python train_transformer.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder/cylinder_transformer_pretrained.pdparams EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder/cylinder_pretrained.pdparams
+
+
+
+
+
+
+
python train_transformer.py mode=export EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder/cylinder_pretrained.pdparams
+
+
+
+
python train_transformer.py mode=export EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/cylinder/cylinder_pretrained.pdparams
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_training.hdf5 --create-dirs -o ./datasets/cylinder_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/cylinder_valid.hdf5 --create-dirs -o ./datasets/cylinder_valid.hdf5
+python train_transformer.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
模型MSE
cylinder_transformer_pretrained.pdparams1.093
+

1. 背景简介

+

圆柱绕流问题可以应用于很多领域。例如,在工业设计中,它可以被用来模拟和优化流体在各种设备中的流动,如风力发电机、汽车和飞机的流体动力学性能等。在环保领域,圆柱绕流问题也有应用,如预测和控制河流的洪水、研究污染物的扩散等。此外,在工程实践中,如流体动力学、流体静力学、热交换、空气动力学等领域,圆柱绕流问题也具有实际意义。

+

2D Flow Around a Cylinder,中文名称可译作“2维圆柱绕流”,是指二维圆柱低速定常绕流的流型只与 \(Re\) 数有关。在 \(Re \le 1\) 时,流场中的惯性力与粘性力相比居次要地位,圆柱上下游的流线前后对称,阻力系数近似与 \(Re\) 成反比(阻力系数为 10~60),此 \(Re\) 数范围的绕流称为斯托克斯区;随着 \(Re\) 的增大,圆柱上下游的流线逐渐失去对称性。

+

2. 问题定义

+

质量守恒:

+
\[ +\frac{\partial u}{\partial x} + \frac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\frac{\partial u}{\partial t} + u\frac{\partial u}{\partial x} + v\frac{\partial u}{\partial y} = -\frac{1}{\rho}\frac{\partial p}{\partial x} + \nu(\frac{\partial ^2 u}{\partial x ^2} + \frac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\frac{\partial v}{\partial t} + u\frac{\partial v}{\partial x} + v\frac{\partial v}{\partial y} = -\frac{1}{\rho}\frac{\partial p}{\partial y} + \nu(\frac{\partial ^2 v}{\partial x ^2} + \frac{\partial ^2 v}{\partial y ^2}) +\]
+

令:

+

\(t^* = \frac{L}{U_0}\)

+

\(x^*=y^* = L\)

+

\(u^*=v^* = U_0\)

+

\(p^* = \rho {U_0}^2\)

+

定义:

+

无量纲时间 \(\tau = \frac{t}{t^*}\)

+

无量纲坐标 \(x:X = \frac{x}{x^*}\);无量纲坐标 \(y:Y = \frac{y}{y^*}\)

+

无量纲速度 \(x:U = \frac{u}{u^*}\);无量纲速度 \(y:V = \frac{v}{u^*}\)

+

无量纲压力 \(P = \frac{p}{p^*}\)

+

雷诺数 \(Re = \frac{L U_0}{\nu}\)

+

则可获得如下无量纲Navier-Stokes方程,施加于流体域内部:

+

质量守恒:

+
\[ +\frac{\partial U}{\partial X} + \frac{\partial U}{\partial Y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\frac{\partial U}{\partial \tau} + U\frac{\partial U}{\partial X} + V\frac{\partial U}{\partial Y} = -\frac{\partial P}{\partial X} + \frac{1}{Re}(\frac{\partial ^2 U}{\partial X^2} + \frac{\partial ^2 U}{\partial Y^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\frac{\partial V}{\partial \tau} + U\frac{\partial V}{\partial X} + V\frac{\partial V}{\partial Y} = -\frac{\partial P}{\partial Y} + \frac{1}{Re}(\frac{\partial ^2 V}{\partial X^2} + \frac{\partial ^2 V}{\partial Y^2}) +\]
+

对于流体域边界和流体域内部圆周边界,则需施加 Dirichlet 边界条件:

+

流体域入口边界:

+
\[ +u=1, v=0 +\]
+

圆周边界:

+
\[ +u=0, v=0 +\]
+

流体域出口边界:

+
\[ +p=0 +\]
+

3. 问题求解

+

接下来开始讲解如何基于 PaddleScience 代码,用深度学习的方法求解该问题。本案例基于论文 Transformers for Modeling Physical Systems 方法进行求解,关于该方法的理论部分请参考此文档原论文。接下来首先会对使用的数据集进行介绍,然后对该方法两个训练步骤(Embedding 模型训练、Transformer 模型训练)的监督约束构建、模型构建等进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集采用了 Transformer-Physx 中提供的数据。该数据集中的数据使用 OpenFOAM 求得,每个时间步大小为0.5,\(Re\) 从以下范围中随机选取:

+
\[Re \sim(100, 750)\]
+

数据集的划分如下:

+ + + + + + + + + + + + + + + + + + + + + + + +
数据集流场仿真的数量时间步的数量下载地址
训练集27400cylinder_training.hdf5
验证集6400cylinder_valid.hdf5
+

数据集官网为:https://zenodo.org/record/5148524#.ZDe77-xByrc

+

3.2 Embedding 模型

+

首先展示代码中定义的各个参数变量,每个参数的具体含义会在下面使用到时进行解释。

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
weights = (10.0 * (cfg.TRAIN_BLOCK_SIZE - 1), 10.0 * cfg.TRAIN_BLOCK_SIZE)
+regularization_key = "k_matrix"
+
+

3.2.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
train_dataloader_cfg = {
+    "dataset": {
+        "name": "CylinderDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 16,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 CylinderDataset,另外还指定了该类初始化时参数的取值:

+
    +
  1. file_path:代表训练数据集的文件路径,指定为变量 train_file_path 的值;
  2. +
  3. input_keys:代表模型输入数据的变量名称,此处填入变量 input_keys
  4. +
  5. label_keys:代表真实标签的变量名称,此处填入变量 output_keys
  6. +
  7. block_size:代表使用多长的时间步进行训练,指定为变量 train_block_size 的值;
  8. +
  9. stride:代表连续的两个训练样本之间的时间步间隔,指定为16;
  10. +
  11. weight_dict:代表模型输出各个变量与真实标签损失函数的权重,此处使用 output_keysweights 生成。
  12. +
+

"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,另外还指定了该类初始化时参数 drop_lastshuffle 均为 True

+

train_dataloader_cfg 还定义了 batch_sizenum_workers 的值。

+

定义监督约束的代码如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELossWithL2Decay(
+        regularization_dict={
+            regularization_key: 1.0e-2 * (cfg.TRAIN_BLOCK_SIZE - 1)
+        }
+    ),
+    {
+        key: lambda out, k=key: out[k]
+        for key in cfg.MODEL.output_keys + (regularization_key,)
+    },
+    name="Sup",
+)
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用带有 L2Decay 的 MSELoss,类名为 MSELossWithL2Decayregularization_dict 设置了正则化的变量名称和对应的权重;

+

第三个参数表示在训练时如何计算需要被约束的中间变量,此处我们约束的变量就是网络的输出;

+

第四个参数是约束条件的名字,方便后续对其索引。此处命名为 "Sup"。

+

3.2.2 模型构建

+

在该案例中,Embedding 模型使用了卷积神经网络实现 Embedding 模型,如下图所示。

+
+

cylinder-embedding +

+
Embedding 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
model = ppsci.arch.CylinderEmbedding(
+    cfg.MODEL.input_keys,
+    cfg.MODEL.output_keys + (regularization_key,),
+    data_mean,
+    data_std,
+)
+
+

其中,CylinderEmbedding 的前两个参数在前文中已有描述,这里不再赘述,网络模型的第三、四个参数是训练数据集的均值和方差,用于归一化输入数据。计算均值、方差的的代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
def get_mean_std(data: np.ndarray, visc: np.ndarray):
+    mean = np.asarray(
+        [
+            np.mean(data[:, :, 0]),
+            np.mean(data[:, :, 1]),
+            np.mean(data[:, :, 2]),
+            np.mean(visc),
+        ]
+    ).reshape(1, 4, 1, 1)
+    std = np.asarray(
+        [
+            np.std(data[:, :, 0]),
+            np.std(data[:, :, 1]),
+            np.std(data[:, :, 2]),
+            np.std(visc),
+        ]
+    ).reshape(1, 4, 1, 1)
+    return mean, std
+
+

3.2.3 学习率与优化器构建

+

本案例中使用的学习率方法为 ExponentialDecay,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
# init optimizer and lr scheduler
+clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    iters_per_epoch=ITERS_PER_EPOCH,
+    decay_steps=ITERS_PER_EPOCH,
+    **cfg.TRAIN.lr_scheduler,
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.2.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "CylinderDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 32,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用 ppsci.metric.MSE

+

3.2.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/cylinder/2d_unsteady/transformer_physx/train_enn.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=True,
+    eval_freq=50,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.3 Transformer 模型

+

上文介绍了如何构建 Embedding 模型的训练、评估,在本节中将介绍如何使用训练好的 Embedding 模型训练 Transformer 模型。因为训练 Transformer 模型的步骤与训练 Embedding 模型的步骤基本相似,因此本节在两者的重复部分的各个参数不再详细介绍。首先将代码中定义的各个参数变量展示如下,每个参数的具体含义会在下面使用到时进行解释。

+
examples/cylinder/2d_unsteady/transformer_physx/conf/transformer.yaml
23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
# general settings
+mode: train # running mode: train/eval
+seed: 42
+output_dir: ${hydra:run.dir}
+TRAIN_BLOCK_SIZE: 16
+VALID_BLOCK_SIZE: 256
+TRAIN_FILE_PATH: ./datasets/cylinder_training.hdf5
+VALID_FILE_PATH: ./datasets/cylinder_valid.hdf5
+log_freq: 20
+
+# set working condition
+EMBEDDING_MODEL_PATH: ./outputs_cylinder2d_unsteady_transformer_physx_enn/checkpoints/latest
+
+

3.3.1 约束构建

+

Transformer 模型同样基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
train_dataloader_cfg = {
+    "dataset": {
+        "name": "CylinderDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 4,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

数据加载的各个参数与 Embedding 模型中的基本一致,不再赘述。需要说明的是由于 Transformer 模型训练的输入数据是 Embedding 模型 Encoder 模块的输出数据,因此我们将训练好的 Embedding 模型作为 CylinderDataset 的一个参数,在初始化时首先将训练数据映射到编码空间。

+

定义监督约束的代码如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
87
+88
+89
+90
+91
+92
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.3.2 模型构建

+

在该案例中,Transformer 模型的输入输出都是编码空间中的向量,使用的 Transformer 结构如下:

+
+

cylinder_transformer +

+
Transformer 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+

PhysformerGPT2 除了需要填入 input_keysoutput_keys 外,还需要设置 Transformer 模型的层数 num_layers、上下文的大小 num_ctx、输入的 Embedding 向量的长度 embed_size、多头注意力机制的参数 num_heads,在这里填入的数值为6、16、128、4。

+

3.3.3 学习率与优化器构建

+

本案例中使用的学习率方法为 CosineWarmRestarts,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
# init optimizer and lr scheduler
+clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+    iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.3.4 评估器构建

+

训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。用 PaddleScience 代码表示如下:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "CylinderDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

3.3.5 可视化器构建

+

本案例中可以通过构建可视化器在模型评估时将评估结果可视化出来,由于 Transformer 模型的输出数据是预测的编码空间的数据无法直接进行可视化,因此需要额外将输出数据使用 Embedding 网络的 Decoder 模块变换到物理状态空间。

+

在本文中首先定义了对 Transformer 模型输出数据变换到物理状态空间的代码:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
def build_embedding_model(embedding_model_path: str) -> ppsci.arch.CylinderEmbedding:
+    input_keys = ("states", "visc")
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.CylinderEmbedding(
+        input_keys, output_keys + (regularization_key,)
+    )
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]:
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+        # pred_states.shape=(B, T, C, H, W)
+        return pred_states
+
+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+output_transform = OutputTransform(embedding_model)
+
+

可以看到,程序首先载入了训练好的 Embedding 模型,然后在 OutputTransform__call__ 函数内实现了编码向量到物理状态空间的变换。

+

在定义好了以上代码之后,就可以实现可视化器代码的构建了:

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
visualizer = {
+    "visualize_states": ppsci.visualize.Visualizer2DPlot(
+        vis_datas,
+        {
+            "target_ux": lambda d: d["states"][:, :, 0],
+            "pred_ux": lambda d: output_transform(d)[:, :, 0],
+            "target_uy": lambda d: d["states"][:, :, 1],
+            "pred_uy": lambda d: output_transform(d)[:, :, 1],
+            "target_p": lambda d: d["states"][:, :, 2],
+            "preds_p": lambda d: output_transform(d)[:, :, 2],
+        },
+        batch_size=1,
+        num_timestamps=10,
+        stride=20,
+        xticks=np.linspace(-2, 14, 9),
+        yticks=np.linspace(-4, 4, 5),
+        prefix="result_states",
+    )
+}
+
+

首先使用上文中的 mse_validator 中的数据集进行可视化,另外还引入了 vis_data_nums 变量用于控制需要可视化样本的数量。最后通过 VisualizerScatter3D 构建可视化器。

+

3.3.6 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step2: training a transformer model, based on frozen pretrained embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+from typing import Dict
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.arch import base
+from ppsci.utils import logger
+from ppsci.utils import save_load
+
+
+def build_embedding_model(embedding_model_path: str) -> ppsci.arch.CylinderEmbedding:
+    input_keys = ("states", "visc")
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.CylinderEmbedding(
+        input_keys, output_keys + (regularization_key,)
+    )
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]:
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+        # pred_states.shape=(B, T, C, H, W)
+        return pred_states
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 4,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(constraint["Sup"].data_loader)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+        iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:],
+    }
+
+    visualizer = {
+        "visualize_states": ppsci.visualize.Visualizer2DPlot(
+            vis_datas,
+            {
+                "target_ux": lambda d: d["states"][:, :, 0],
+                "pred_ux": lambda d: output_transform(d)[:, :, 0],
+                "target_uy": lambda d: d["states"][:, :, 1],
+                "pred_uy": lambda d: output_transform(d)[:, :, 1],
+                "target_p": lambda d: d["states"][:, :, 2],
+                "preds_p": lambda d: output_transform(d)[:, :, 2],
+            },
+            batch_size=1,
+            num_timestamps=10,
+            stride=20,
+            xticks=np.linspace(-2, 14, 9),
+            yticks=np.linspace(-4, 4, 5),
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # directly evaluate pretrained model(optional)
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:],
+    }
+
+    visualizer = {
+        "visulzie_states": ppsci.visualize.Visualizer2DPlot(
+            vis_datas,
+            {
+                "target_ux": lambda d: d["states"][:, :, 0],
+                "pred_ux": lambda d: output_transform(d)[:, :, 0],
+                "target_uy": lambda d: d["states"][:, :, 1],
+                "pred_uy": lambda d: output_transform(d)[:, :, 1],
+                "target_p": lambda d: d["states"][:, :, 2],
+                "preds_p": lambda d: output_transform(d)[:, :, 2],
+            },
+            batch_size=1,
+            num_timestamps=10,
+            stride=20,
+            xticks=np.linspace(-2, 14, 9),
+            yticks=np.linspace(-4, 4, 5),
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction for pretrained model(optional)
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    model_cfg = {
+        **cfg.MODEL,
+        "embedding_model": embedding_model,
+        "input_keys": ["states"],
+        "output_keys": ["pred_states"],
+    }
+    model = ppsci.arch.PhysformerGPT2(**model_cfg)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            "states": InputSpec([1, 255, 3, 64, 128], "float32", name="states"),
+            "visc": InputSpec([1, 1], "float32", name="visc"),
+        },
+    ]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy import python_infer
+
+    predictor = python_infer.GeneralPredictor(cfg)
+
+    dataset_cfg = {
+        "name": "CylinderDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+    }
+
+    dataset = ppsci.data.dataset.build_dataset(dataset_cfg)
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, :-1],
+        "visc": dataset.visc[: cfg.VIS_DATA_NUMS],
+    }
+
+    output_dict = predictor.predict(input_dict)
+
+    # mapping data to cfg.INFER.output_keys
+    output_keys = ["pred_states"]
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+    for i in range(cfg.VIS_DATA_NUMS):
+        ppsci.visualize.plot.save_plot_from_2d_dict(
+            f"./cylinder_transformer_pred_{i}",
+            {
+                "pred_ux": output_dict["pred_states"][i][:, 0],
+                "pred_uy": output_dict["pred_states"][i][:, 1],
+                "pred_p": output_dict["pred_states"][i][:, 2],
+            },
+            ("pred_ux", "pred_uy", "pred_p"),
+            10,
+            20,
+            np.linspace(-2, 14, 9),
+            np.linspace(-4, 4, 5),
+        )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="transformer.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
examples/cylinder/2d_unsteady/transformer_physx/train_transformer.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step2: training a transformer model, based on frozen pretrained embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+from typing import Dict
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.arch import base
+from ppsci.utils import logger
+from ppsci.utils import save_load
+
+
+def build_embedding_model(embedding_model_path: str) -> ppsci.arch.CylinderEmbedding:
+    input_keys = ("states", "visc")
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.CylinderEmbedding(
+        input_keys, output_keys + (regularization_key,)
+    )
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]:
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+        # pred_states.shape=(B, T, C, H, W)
+        return pred_states
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 4,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(constraint["Sup"].data_loader)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+        iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:],
+    }
+
+    visualizer = {
+        "visualize_states": ppsci.visualize.Visualizer2DPlot(
+            vis_datas,
+            {
+                "target_ux": lambda d: d["states"][:, :, 0],
+                "pred_ux": lambda d: output_transform(d)[:, :, 0],
+                "target_uy": lambda d: d["states"][:, :, 1],
+                "pred_uy": lambda d: output_transform(d)[:, :, 1],
+                "target_p": lambda d: d["states"][:, :, 2],
+                "preds_p": lambda d: output_transform(d)[:, :, 2],
+            },
+            batch_size=1,
+            num_timestamps=10,
+            stride=20,
+            xticks=np.linspace(-2, 14, 9),
+            yticks=np.linspace(-4, 4, 5),
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # directly evaluate pretrained model(optional)
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "CylinderDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:],
+    }
+
+    visualizer = {
+        "visulzie_states": ppsci.visualize.Visualizer2DPlot(
+            vis_datas,
+            {
+                "target_ux": lambda d: d["states"][:, :, 0],
+                "pred_ux": lambda d: output_transform(d)[:, :, 0],
+                "target_uy": lambda d: d["states"][:, :, 1],
+                "pred_uy": lambda d: output_transform(d)[:, :, 1],
+                "target_p": lambda d: d["states"][:, :, 2],
+                "preds_p": lambda d: output_transform(d)[:, :, 2],
+            },
+            batch_size=1,
+            num_timestamps=10,
+            stride=20,
+            xticks=np.linspace(-2, 14, 9),
+            yticks=np.linspace(-4, 4, 5),
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction for pretrained model(optional)
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    model_cfg = {
+        **cfg.MODEL,
+        "embedding_model": embedding_model,
+        "input_keys": ["states"],
+        "output_keys": ["pred_states"],
+    }
+    model = ppsci.arch.PhysformerGPT2(**model_cfg)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            "states": InputSpec([1, 255, 3, 64, 128], "float32", name="states"),
+            "visc": InputSpec([1, 1], "float32", name="visc"),
+        },
+    ]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy import python_infer
+
+    predictor = python_infer.GeneralPredictor(cfg)
+
+    dataset_cfg = {
+        "name": "CylinderDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+    }
+
+    dataset = ppsci.data.dataset.build_dataset(dataset_cfg)
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, :-1],
+        "visc": dataset.visc[: cfg.VIS_DATA_NUMS],
+    }
+
+    output_dict = predictor.predict(input_dict)
+
+    # mapping data to cfg.INFER.output_keys
+    output_keys = ["pred_states"]
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+    for i in range(cfg.VIS_DATA_NUMS):
+        ppsci.visualize.plot.save_plot_from_2d_dict(
+            f"./cylinder_transformer_pred_{i}",
+            {
+                "pred_ux": output_dict["pred_states"][i][:, 0],
+                "pred_uy": output_dict["pred_states"][i][:, 1],
+                "pred_p": output_dict["pred_states"][i][:, 2],
+            },
+            ("pred_ux", "pred_uy", "pred_p"),
+            10,
+            20,
+            np.linspace(-2, 14, 9),
+            np.linspace(-4, 4, 5),
+        )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="transformer.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

针对本案例中的问题,模型的预测结果和传统数值微分的结果如下所示,其中 ux、uy 分别代表 x、y方向上的速度,p 代表压力。

+
+

result_states0 +

+
模型预测结果("pred")与传统数值微分结果("target")
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/darcy2d/index.html b/zh/examples/darcy2d/index.html new file mode 100644 index 0000000000..327a081c0a --- /dev/null +++ b/zh/examples/darcy2d/index.html @@ -0,0 +1,4836 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Darcy2D - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-Darcy

+

AI Studio快速体验

+
+
+
+
python darcy2d.py
+
+
+
+
python darcy2d.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/darcy2d/darcy2d_pretrained.pdparams
+
+
+
+
python darcy2d.py mode=export
+
+
+
+
python darcy2d.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
darcy2d_pretrained.pdparamsloss(Residual): 0.36500
MSE.poisson(Residual): 0.00006
+

1. 背景简介

+

Darcy Flow是一个基于达西定律的工具,用于计算液体的流动。在地下水模拟、水文学、水文地质学和石油工程等领域中,Darcy Flow被广泛应用。

+

例如,在石油工程中,Darcy Flow被用来预测和模拟石油在多孔介质中的流动。多孔介质是一种由小颗粒组成的物质,颗粒之间存在空隙。石油会填充这些空隙并在其中流动。通过Darcy Flow,工程师可以预测和控制石油的流动,从而优化石油开采和生产过程。

+

此外,Darcy Flow也被用于研究和预测地下水的流动。例如,在农业领域,通过模拟地下水流动可以预测灌溉对土壤水分的影响,从而优化作物灌溉计划。在城市规划和环境保护中,Darcy Flow也被用来预测和防止地下水污染。

+

2D-Darcy 是达西渗流(Darcy flow)的一种,流体在多孔介质中流动时,渗流速度小,流动服从达西定律,渗流速度和压力梯度之间呈线性关系,这种流动称为线性渗流。

+

2. 问题定义

+

假设达西流模型中,每个位置 \((x,y)\) 上的流速 \(\mathbf{u}\) 和压力 \(p\) 之间满足以下关系式:

+
\[ +\begin{cases} +\begin{aligned} + \mathbf{u}+\nabla p =& 0,(x,y) \in \Omega \\ + \nabla \cdot \mathbf{u} =& f,(x,y) \in \Omega \\ + p(x,y) =& \sin(2 \pi x )\cos(2 \pi y), (x,y) \in \partial \Omega +\end{aligned} +\end{cases} +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 darcy-2d 问题中,每一个已知的坐标点 \((x, y)\) 都有对应的待求解的未知量 \(p\) +,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y)\)\(p\) 的映射函数 \(f: \mathbb{R}^2 \to \mathbb{R}^1\) ,即:

+
\[ +p = f(x, y) +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("x", "y"),输出变量名是 "p",这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数,我们就实例化出了一个拥有 5 层隐藏神经元,每层神经元数为 20 的神经网络模型 model

+

3.2 方程构建

+

由于 2D-Poisson 使用的是 Poisson 方程的2维形式,因此可以直接使用 PaddleScience 内置的 Poisson,指定该类的参数 dim 为2。

+
# set equation
+equation = {"Poisson": ppsci.equation.Poisson(2)}
+
+

3.3 计算域构建

+

本文中 2D darcy 问题作用在以 (0.0, 0.0), (1.0, 1.0) 为对角线的二维矩形区域, +因此可以直接使用 PaddleScience 内置的空间几何 Rectangle 作为计算域。

+
# set geometry
+geom = {"rect": ppsci.geometry.Rectangle((0.0, 0.0), (1.0, 1.0))}
+
+

3.4 约束构建

+

在本案例中,我们使用了两个约束条件在计算域中指导模型的训练分别是作用于采样点上的 darcy 方程约束和作用于边界点上的约束。

+

在定义约束之前,需要给每一种约束指定采样点个数,表示每一种约束在其对应计算域内采样数据的数量,以及通用的采样配置。

+
42
+43
+44
+45
+46
# set dataloader config
+train_dataloader_cfg = {
+    "dataset": "IterableNamedArrayDataset",
+    "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+}
+
+

3.4.1 内部点约束

+

以作用在内部点上的 InteriorConstraint 为例,代码如下:

+
48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
# set constraint
+def poisson_ref_compute_func(_in):
+    return (
+        -8.0
+        * (np.pi**2)
+        * np.sin(2.0 * np.pi * _in["x"])
+        * np.cos(2.0 * np.pi * _in["y"])
+    )
+
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["Poisson"].equations,
+    {"poisson": poisson_ref_compute_func},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": cfg.NPOINT_PDE},
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    name="EQ",
+)
+
+

InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["Poisson"].equations

+

第二个参数是约束变量的目标值,在本问题中我们希望 Poisson 方程产生的结果被优化至与其标准解一致,因此将它的目标值全设为 poisson_ref_compute_func 产生的结果;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["rect"] 即可;

+

第四个参数是在计算域上的采样配置,此处我们使用全量数据点训练,因此 dataset 字段设置为 "IterableNamedArrayDataset" 且 iters_per_epoch 也设置为 1,采样点数 batch_size 设为 9801(表示99x99的采样网格);

+

第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "sum",即我们会将参与计算的所有数据点产生的损失项求和;

+

第六个参数是选择是否在计算域上进行等间隔采样,此处我们选择开启等间隔采样,这样能让训练点均匀分布在计算域上,有利于训练收敛;

+

第七个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

+

3.4.2 边界约束

+

同理,我们还需要构建矩形的四个边界的约束。但与构建 InteriorConstraint 约束不同的是,由于作用区域是边界,因此我们使用 BoundaryConstraint 类,代码如下:

+
67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
bc = ppsci.constraint.BoundaryConstraint(
+    {"p": lambda out: out["p"]},
+    {
+        "p": lambda _in: np.sin(2.0 * np.pi * _in["x"])
+        * np.cos(2.0 * np.pi * _in["y"])
+    },
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": cfg.NPOINT_BC},
+    ppsci.loss.MSELoss("sum"),
+    name="BC",
+)
+
+

BoundaryConstraint 类第一个参数表示我们直接对网络模型的输出结果 out["p"] 作为程序运行时的约束对象;

+

第二个参数是指我们约束对象的真值如何获得,这里我们直接通过其解析解进行计算,定义解析解的代码如下:

+
lambda _in: np.sin(2.0 * np.pi * _in["x"]) * np.cos(2.0 * np.pi * _in["y"])
+
+

BoundaryConstraint 类其他参数的含义与 InteriorConstraint 基本一致,这里不再介绍。

+

在微分方程约束、边界约束、初值约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
78
+79
+80
+81
+82
# wrap constraints together
+constraint = {
+    pde_constraint.name: pde_constraint,
+    bc.name: bc,
+}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用一万轮训练轮数。

+
39
+40
+41
+42
+43
+44
+45
+46
# training settings
+TRAIN:
+  epochs: 10000
+  iters_per_epoch: 1
+  lr_scheduler:
+    epochs: ${TRAIN.epochs}
+    iters_per_epoch: ${TRAIN.iters_per_epoch}
+    max_learning_rate: 1.0e-3
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并配合使用机器学习中常用的 OneCycle 学习率调整策略。

+
84
+85
+86
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.OneCycleLR(**cfg.TRAIN.lr_scheduler)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
# set validator
+residual_validator = ppsci.validate.GeometryValidator(
+    equation["Poisson"].equations,
+    {"poisson": poisson_ref_compute_func},
+    geom["rect"],
+    {
+        "dataset": "NamedArrayDataset",
+        "total_size": cfg.NPOINT_PDE,
+        "batch_size": cfg.EVAL.batch_size.residual_validator,
+        "sampler": {"name": "BatchSampler"},
+    },
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    metric={"MSE": ppsci.metric.MSE()},
+    name="Residual",
+)
+validator = {residual_validator.name: residual_validator}
+
+

3.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文中的输出数据是一个区域内的二维点集,因此我们只需要将评估的输出数据保存成 vtu格式 文件,最后用可视化软件打开查看即可。代码如下:

+
# set visualizer(optional)
+# manually collate input data for visualization,
+vis_points = geom["rect"].sample_interior(
+    cfg.NPOINT_PDE + cfg.NPOINT_BC, evenly=True
+)
+visualizer = {
+    "visualize_p_ux_uy": ppsci.visualize.VisualizerVtu(
+        vis_points,
+        {
+            "p": lambda d: d["p"],
+            "p_ref": lambda d: paddle.sin(2 * np.pi * d["x"])
+            * paddle.cos(2 * np.pi * d["y"]),
+            "p_diff": lambda d: paddle.sin(2 * np.pi * d["x"])
+            * paddle.cos(2 * np.pi * d["y"])
+            - d["p"],
+            "ux": lambda d: jacobian(d["p"], d["x"]),
+            "ux_ref": lambda d: 2
+            * np.pi
+            * paddle.cos(2 * np.pi * d["x"])
+            * paddle.cos(2 * np.pi * d["y"]),
+            "ux_diff": lambda d: jacobian(d["p"], d["x"])
+            - 2
+            * np.pi
+            * paddle.cos(2 * np.pi * d["x"])
+            * paddle.cos(2 * np.pi * d["y"]),
+            "uy": lambda d: jacobian(d["p"], d["y"]),
+            "uy_ref": lambda d: -2
+            * np.pi
+            * paddle.sin(2 * np.pi * d["x"])
+            * paddle.sin(2 * np.pi * d["y"]),
+            "uy_diff": lambda d: jacobian(d["p"], d["y"])
+            - (
+                -2
+                * np.pi
+                * paddle.sin(2 * np.pi * d["x"])
+                * paddle.sin(2 * np.pi * d["y"])
+            ),
+        },
+        prefix="result_p_ux_uy",
+    )
+}
+
+

3.9 模型训练、评估与可视化

+

3.9.1 使用 Adam 训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

3.9.2 使用 L-BFGS 微调[可选]

+

在使用 Adam 优化器训练完毕之后,我们可以将优化器更换成二阶优化器 L-BFGS 继续训练少量轮数(此处我们使用 Adam 优化轮数的 10% 即可),从而进一步提高模型精度。

+
OUTPUT_DIR = cfg.TRAIN.lbfgs.output_dir
+logger.init_logger("ppsci", osp.join(OUTPUT_DIR, f"{cfg.mode}.log"), "info")
+EPOCHS = cfg.TRAIN.epochs // 10
+optimizer_lbfgs = ppsci.optimizer.LBFGS(
+    cfg.TRAIN.lbfgs.learning_rate, cfg.TRAIN.lbfgs.max_iter
+)(model)
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer_lbfgs,
+    None,
+    EPOCHS,
+    cfg.TRAIN.lbfgs.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.lbfgs.eval_during_train,
+    eval_freq=cfg.TRAIN.lbfgs.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+
+提示 +

在常规优化器训练完毕之后,使用 L-BFGS 微调少量轮数的方法,在大多数场景中都可以进一步有效提高模型精度。

+
+

4. 完整代码

+
darcy2d.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"Poisson": ppsci.equation.Poisson(2)}
+
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((0.0, 0.0), (1.0, 1.0))}
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": "IterableNamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+
+    # set constraint
+    def poisson_ref_compute_func(_in):
+        return (
+            -8.0
+            * (np.pi**2)
+            * np.sin(2.0 * np.pi * _in["x"])
+            * np.cos(2.0 * np.pi * _in["y"])
+        )
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["Poisson"].equations,
+        {"poisson": poisson_ref_compute_func},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": cfg.NPOINT_PDE},
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        name="EQ",
+    )
+
+    bc = ppsci.constraint.BoundaryConstraint(
+        {"p": lambda out: out["p"]},
+        {
+            "p": lambda _in: np.sin(2.0 * np.pi * _in["x"])
+            * np.cos(2.0 * np.pi * _in["y"])
+        },
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": cfg.NPOINT_BC},
+        ppsci.loss.MSELoss("sum"),
+        name="BC",
+    )
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        bc.name: bc,
+    }
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.OneCycleLR(**cfg.TRAIN.lr_scheduler)()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    # set validator
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["Poisson"].equations,
+        {"poisson": poisson_ref_compute_func},
+        geom["rect"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": cfg.NPOINT_PDE,
+            "batch_size": cfg.EVAL.batch_size.residual_validator,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer(optional)
+    # manually collate input data for visualization,
+    vis_points = geom["rect"].sample_interior(
+        cfg.NPOINT_PDE + cfg.NPOINT_BC, evenly=True
+    )
+    visualizer = {
+        "visualize_p_ux_uy": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {
+                "p": lambda d: d["p"],
+                "p_ref": lambda d: paddle.sin(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "p_diff": lambda d: paddle.sin(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"])
+                - d["p"],
+                "ux": lambda d: jacobian(d["p"], d["x"]),
+                "ux_ref": lambda d: 2
+                * np.pi
+                * paddle.cos(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "ux_diff": lambda d: jacobian(d["p"], d["x"])
+                - 2
+                * np.pi
+                * paddle.cos(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "uy": lambda d: jacobian(d["p"], d["y"]),
+                "uy_ref": lambda d: -2
+                * np.pi
+                * paddle.sin(2 * np.pi * d["x"])
+                * paddle.sin(2 * np.pi * d["y"]),
+                "uy_diff": lambda d: jacobian(d["p"], d["y"])
+                - (
+                    -2
+                    * np.pi
+                    * paddle.sin(2 * np.pi * d["x"])
+                    * paddle.sin(2 * np.pi * d["y"])
+                ),
+            },
+            prefix="result_p_ux_uy",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+    # fine-tuning pretrained model with L-BFGS
+    OUTPUT_DIR = cfg.TRAIN.lbfgs.output_dir
+    logger.init_logger("ppsci", osp.join(OUTPUT_DIR, f"{cfg.mode}.log"), "info")
+    EPOCHS = cfg.TRAIN.epochs // 10
+    optimizer_lbfgs = ppsci.optimizer.LBFGS(
+        cfg.TRAIN.lbfgs.learning_rate, cfg.TRAIN.lbfgs.max_iter
+    )(model)
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        OUTPUT_DIR,
+        optimizer_lbfgs,
+        None,
+        EPOCHS,
+        cfg.TRAIN.lbfgs.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.lbfgs.eval_during_train,
+        eval_freq=cfg.TRAIN.lbfgs.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"Poisson": ppsci.equation.Poisson(2)}
+
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((0.0, 0.0), (1.0, 1.0))}
+
+    # set constraint
+    def poisson_ref_compute_func(_in):
+        return (
+            -8.0
+            * (np.pi**2)
+            * np.sin(2.0 * np.pi * _in["x"])
+            * np.cos(2.0 * np.pi * _in["y"])
+        )
+
+    # set validator
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["Poisson"].equations,
+        {"poisson": poisson_ref_compute_func},
+        geom["rect"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": cfg.NPOINT_PDE,
+            "batch_size": cfg.EVAL.batch_size.residual_validator,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer
+    # manually collate input data for visualization,
+    vis_points = geom["rect"].sample_interior(
+        cfg.NPOINT_PDE + cfg.NPOINT_BC, evenly=True
+    )
+    visualizer = {
+        "visualize_p_ux_uy": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {
+                "p": lambda d: d["p"],
+                "p_ref": lambda d: paddle.sin(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "p_diff": lambda d: paddle.sin(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"])
+                - d["p"],
+                "ux": lambda d: jacobian(d["p"], d["x"]),
+                "ux_ref": lambda d: 2
+                * np.pi
+                * paddle.cos(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "ux_diff": lambda d: jacobian(d["p"], d["x"])
+                - 2
+                * np.pi
+                * paddle.cos(2 * np.pi * d["x"])
+                * paddle.cos(2 * np.pi * d["y"]),
+                "uy": lambda d: jacobian(d["p"], d["y"]),
+                "uy_ref": lambda d: -2
+                * np.pi
+                * paddle.sin(2 * np.pi * d["x"])
+                * paddle.sin(2 * np.pi * d["y"]),
+                "uy_diff": lambda d: jacobian(d["p"], d["y"])
+                - (
+                    -2
+                    * np.pi
+                    * paddle.sin(2 * np.pi * d["x"])
+                    * paddle.sin(2 * np.pi * d["y"])
+                ),
+            },
+            prefix="result_p_ux_uy",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((0.0, 0.0), (1.0, 1.0))}
+    # manually collate input data for visualization,
+    input_dict = geom["rect"].sample_interior(
+        cfg.NPOINT_PDE + cfg.NPOINT_BC, evenly=True
+    )
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+    ppsci.visualize.save_vtu_from_dict(
+        "./visual/darcy2d.vtu",
+        {**input_dict, **output_dict},
+        input_dict.keys(),
+        cfg.MODEL.output_keys,
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="darcy2d.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下方展示了模型对正方形计算域中每个点的压力\(p(x,y)\)、x(水平)方向流速\(u(x,y)\)、y(垂直)方向流速\(v(x,y)\)的预测结果、参考结果以及两者之差。

+
+

darcy 2d +

+
左:预测压力 p,中:参考压力 p,右:压力差
+

darcy 2d +

+
左:预测x方向流速 p,中:参考x方向流速 p,右:x方向流速差
+

darcy 2d +

+
左:预测y方向流速 p,中:参考y方向流速 p,右:y方向流速差
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/deepcfd/index.html b/zh/examples/deepcfd/index.html new file mode 100644 index 0000000000..bb6a553f52 --- /dev/null +++ b/zh/examples/deepcfd/index.html @@ -0,0 +1,4858 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DeepCFD - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

DeepCFD(Deep Computational Fluid Dynamics)

+
+
+
+
# linux
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataX.pkl
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataY.pkl
+# windows
+# curl --create-dirs -o ./datasets/dataX.pkl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataX.pkl
+# curl --create-dirs -o ./datasets/dataX.pkl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataY.pkl
+python deepcfd.py
+
+
+
+
# linux
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataX.pkl
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataY.pkl
+# windows
+# curl --create-dirs -o ./datasets/dataX.pkl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataX.pkl
+# curl --create-dirs -o ./datasets/dataX.pkl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataY.pkl
+python deepcfd.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/deepcfd/deepcfd_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
deepcfd_pretrained.pdparamsMSE.Total_MSE(mse_validator): 1.92947
MSE.Ux_MSE(mse_validator): 0.70684
MSE.Uy_MSE(mse_validator): 0.21337
MSE.p_MSE(mse_validator): 1.00926
+

1. 背景简介

+

计算流体力学(Computational fluid dynamics, CFD)模拟通过求解 Navier-Stokes 方程(N-S 方程),可以获得流体的各种物理量的分布,如密度、压力和速度等。在微电子系统、土木工程和航空航天等领域应用广泛。

+

在某些复杂的应用场景中,如机翼优化和流体与结构相互作用方面,需要使用千万级甚至上亿的网格对问题进行建模(如下图所示,下图展示了 F-18 战斗机的全机内外流一体结构化网格模型),导致 CFD 的计算量非常巨大。因此,目前亟需发展出一种相比于传统 CFD 方法更高效,且可以保持计算精度的方法。

+
+

result_states0 +

+
F-18 战斗机的全机内外流一体结构化网格模型
+
+

2. 问题定义

+

Navier-Stokes 方程是用于描述流体运动的方程,它的二维形式如下,

+

质量守恒:

+
\[\nabla \cdot \bf{u}=0\]
+

动量守恒:

+
\[\rho(\frac{\partial}{\partial t} + \bf{u} \cdot div ) \bf{u} = - \nabla p + - \nabla \tau + \bf{f}\]
+

其中 \(\bf{u}\) 是速度场(具有 x 和 y 两个维度),\(\rho\) 是密度, \(p\) 是压强场,\(\bf{f}\) 是体积力(例如重力)。

+

假设满足非均匀稳态流体条件,方程可去掉时间相关项,并将 \(\bf{u}\) 分解为速度分量 \(u_x\)\(u_y\) ,动量方程可重写成:

+
\[u_x\frac{\partial u_x}{\partial x} + u_y\frac{\partial u_x}{\partial y} = - \frac{1}{\rho}\frac{\partial p}{\partial x} + \nu \nabla^2 u_x + g_x\]
+
\[u_x\frac{\partial u_y}{\partial x} + u_y\frac{\partial u_y}{\partial y} = - \frac{1}{\rho}\frac{\partial p}{\partial y} + \nu \nabla^2 u_y + g_y\]
+

其中 \(g\) 代表重力加速度,\(\nu\) 代表流体的动力粘度。

+

3. 问题求解

+

上述问题通常可使用 OpenFOAM 进行传统数值方法的求解,但计算量很大,接下来开始讲解如何基于 PaddleScience 代码,用深度学习的方法求解该问题。

+

本案例基于论文 Ribeiro M D, Rehman A, Ahmed S, et al. DeepCFD: Efficient steady-state laminar flow approximation with deep convolutional neural networks 的方法进行求解,关于该方法的理论部分请参考原论文。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

该数据集中的数据使用 OpenFOAM 求得。数据集有两个文件 dataX 和 dataY。dataX 包含 981 个通道流样本几何形状的输入信息,dataY 包含对应的 OpenFOAM 求解结果。

+

运行本问题代码前请按照下方命令下载 dataXdataY

+
wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataX.pkl
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepCFD/dataY.pkl
+
+

dataX 和 dataY 都具有相同的维度(Ns,Nc,Nx,Ny),其中第一轴是样本数(Ns),第二轴是通道数(Nc),第三和第四轴分别是 x 和 y 中的元素数量(Nx 和 Ny)。在输入数据 dataX 中,第一通道是计算域中障碍物的SDF(Signed distance function),第二通道是流动区域的标签,第三通道是计算域边界的 SDF。在输出数据 dataY 中,第一个通道是水平速度分量(Ux),第二个通道是垂直速度分量(Uy),第三个通道是流体压强(p)。

+

数据集原始下载地址为:https://zenodo.org/record/3666056/files/DeepCFD.zip?download=1

+

我们将数据集以 7:3 的比例划分为训练集和验证集,代码如下:

+
examples/deepcfd/deepcfd.py
# set random seed for reproducibility
+ppsci.utils.misc.set_random_seed(cfg.seed)
+# initialize logger
+logger.init_logger("ppsci", os.path.join(cfg.output_dir, "train.log"), "info")
+
+# initialize datasets
+with open(cfg.DATAX_PATH, "rb") as file:
+    x = pickle.load(file)
+with open(cfg.DATAY_PATH, "rb") as file:
+    y = pickle.load(file)
+
+# split dataset to train dataset and test dataset
+train_dataset, test_dataset = split_tensors(x, y, ratio=cfg.SLIPT_RATIO)
+train_x, train_y = train_dataset
+test_x, test_y = test_dataset
+
+

3.2 模型构建

+

在上述问题中,我们确定了输入为 input,输出为 output,按照论文所述,我们使用含有 3 个 encoder 和 decoder 的 UNetEx 网络来创建模型。

+

模型的输入包含了障碍物的 SDF(Signed distance function)、流动区域的标签以及计算域边界的 SDF。模型的输出包含了水平速度分量(Ux),垂直速度分量(Uy)以及流体压强(p)。

+
+

DeepCFD +

+
DeepCFD网络结构
+
+

模型创建用 PaddleScience 代码表示如下:

+
examples/deepcfd/deepcfd.py
# initialize model
+model = ppsci.arch.UNetEx(**cfg.MODEL)
+
+

3.3 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/deepcfd/deepcfd.py
# define loss
+def loss_expr(
+    output_dict: Dict[str, np.ndarray],
+    label_dict: Dict[str, np.ndarray] = None,
+    weight_dict: Dict[str, np.ndarray] = None,
+) -> float:
+    output = output_dict["output"]
+    y = label_dict["output"]
+    loss_u = (output[:, 0:1, :, :] - y[:, 0:1, :, :]) ** 2
+    loss_v = (output[:, 1:2, :, :] - y[:, 1:2, :, :]) ** 2
+    loss_p = (output[:, 2:3, :, :] - y[:, 2:3, :, :]).abs()
+    loss = (loss_u + loss_v + loss_p) / CHANNELS_WEIGHTS
+    return {"output": loss.sum()}
+
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"input": train_x},
+            "label": {"output": train_y},
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.FunctionalLoss(loss_expr),
+    name="sup_constraint",
+)
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里填入相关数据的变量名。

+

第二个参数是损失函数的定义,这里使用自定义的损失函数,分别计算 Ux 和 Uy 的均方误差,以及 p 的标准差,然后三者加权求和。

+

第三个参数是约束条件的名字,方便后续对其索引。此次命名为 "sup_constraint"。

+

在监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
examples/deepcfd/deepcfd.py
# manually build constraint
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.4 超参数设定

+

接下来需要在配置文件中指定训练轮数,此处我们按实验经验,使用一千轮训练轮数。

+
examples/deepcfd/conf/deepcfd.yaml
47
+48
+49
+50
+51
# training settings
+TRAIN:
+  epochs: 1000
+  learning_rate: 0.001
+  weight_decay: 0.005
+
+

3.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,学习率设置为 0.001,权重衰减设置为 0.005。

+
examples/deepcfd/deepcfd.py
# initialize Adam optimizer
+optimizer = ppsci.optimizer.Adam(
+    cfg.TRAIN.learning_rate, weight_decay=cfg.TRAIN.weight_decay
+)(model)
+
+

3.6 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集评估当前模型的训练情况,我们使用 ppsci.validate.SupervisedValidator 构建评估器。

+
examples/deepcfd/deepcfd.py
# manually build validator
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "NamedArrayDataset",
+        "input": {"input": test_x},
+        "label": {"output": test_y},
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+}
+
+def metric_expr(
+    output_dict: Dict[str, np.ndarray],
+    label_dict: Dict[str, np.ndarray] = None,
+    weight_dict: Dict[str, np.ndarray] = None,
+) -> Dict[str, float]:
+    output = output_dict["output"]
+    y = label_dict["output"]
+    total_mse = ((output - y) ** 2).sum() / len(test_x)
+    ux_mse = ((output[:, 0, :, :] - test_y[:, 0, :, :]) ** 2).sum() / len(test_x)
+    uy_mse = ((output[:, 1, :, :] - test_y[:, 1, :, :]) ** 2).sum() / len(test_x)
+    p_mse = ((output[:, 2, :, :] - test_y[:, 2, :, :]) ** 2).sum() / len(test_x)
+    return {
+        "Total_MSE": total_mse,
+        "Ux_MSE": ux_mse,
+        "Uy_MSE": uy_mse,
+        "p_MSE": p_mse,
+    }
+
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.FunctionalLoss(loss_expr),
+    {"output": lambda out: out["output"]},
+    {"MSE": ppsci.metric.FunctionalMetric(metric_expr)},
+    name="mse_validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

评价指标 metric 这里自定义了四个指标 Total_MSE、Ux_MSE、Uy_MSE 和 p_MSE。

+

其余配置与 约束构建 的设置类似。

+

3.7 模型训练、评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/deepcfd/deepcfd.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    seed=cfg.seed,
+    validator=validator,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+
+# train model
+solver.train()
+
+# evaluate after finished training
+solver.eval()
+
+

3.8 结果可视化

+

使用 matplotlib 绘制相同输入参数时的 OpenFOAM 和 DeepCFD 的计算结果,进行对比。这里绘制了验证集第 0 个数据的计算结果。

+
examples/deepcfd/deepcfd.py
PLOT_DIR = os.path.join(cfg.output_dir, "visual")
+os.makedirs(PLOT_DIR, exist_ok=True)
+
+# visualize prediction after finished training
+predict_and_save_plot(test_x, test_y, 0, solver, PLOT_DIR)
+
+

4. 完整代码

+
examples/deepcfd/deepcfd.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import pickle
+from typing import Dict
+from typing import List
+from typing import Tuple
+
+import hydra
+import numpy as np
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def split_tensors(
+    *tensors: List[np.array], ratio: float
+) -> Tuple[List[np.array], List[np.array]]:
+    """Split tensors to two parts.
+
+    Args:
+        tensors (List[np.array]): Non-empty tensor list.
+        ratio (float): Split ratio. For example, tensor list A is split to A1 and A2. len(A1) / len(A) = ratio.
+    Returns:
+        Tuple[List[np.array], List[np.array]]: Split tensors.
+    """
+    if len(tensors) == 0:
+        raise ValueError("Tensors shouldn't be empty.")
+
+    split1, split2 = [], []
+    count = len(tensors[0])
+    for tensor in tensors:
+        if len(tensor) != count:
+            raise ValueError("The size of tensor should be same.")
+        x = int(len(tensor) * ratio)
+        split1.append(tensor[:x])
+        split2.append(tensor[x:])
+
+    if len(tensors) == 1:
+        split1, split2 = split1[0], split2[0]
+    return split1, split2
+
+
+def predict_and_save_plot(
+    x: np.ndarray, y: np.ndarray, index: int, solver: ppsci.solver.Solver, plot_dir: str
+):
+    """Make prediction and save visualization of result.
+
+    Args:
+        x (np.ndarray): Input of test dataset.
+        y (np.ndarray): Output of test dataset.
+        index (int): Index of data to visualizer.
+        solver (ppsci.solver.Solver): Trained solver.
+        plot_dir (str): Directory to save plot.
+    """
+    min_u = np.min(y[index, 0, :, :])
+    max_u = np.max(y[index, 0, :, :])
+
+    min_v = np.min(y[index, 1, :, :])
+    max_v = np.max(y[index, 1, :, :])
+
+    min_p = np.min(y[index, 2, :, :])
+    max_p = np.max(y[index, 2, :, :])
+
+    output = solver.predict({"input": x}, return_numpy=True)
+    pred_y = output["output"]
+    error = np.abs(y - pred_y)
+
+    min_error_u = np.min(error[index, 0, :, :])
+    max_error_u = np.max(error[index, 0, :, :])
+
+    min_error_v = np.min(error[index, 1, :, :])
+    max_error_v = np.max(error[index, 1, :, :])
+
+    min_error_p = np.min(error[index, 2, :, :])
+    max_error_p = np.max(error[index, 2, :, :])
+
+    plt.figure()
+    fig = plt.gcf()
+    fig.set_size_inches(15, 10)
+    plt.subplot(3, 3, 1)
+    plt.title("OpenFOAM", fontsize=18)
+    plt.imshow(
+        np.transpose(y[index, 0, :, :]),
+        cmap="jet",
+        vmin=min_u,
+        vmax=max_u,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.ylabel("Ux", fontsize=18)
+    plt.subplot(3, 3, 2)
+    plt.title("DeepCFD", fontsize=18)
+    plt.imshow(
+        np.transpose(pred_y[index, 0, :, :]),
+        cmap="jet",
+        vmin=min_u,
+        vmax=max_u,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.subplot(3, 3, 3)
+    plt.title("Error", fontsize=18)
+    plt.imshow(
+        np.transpose(error[index, 0, :, :]),
+        cmap="jet",
+        vmin=min_error_u,
+        vmax=max_error_u,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+
+    plt.subplot(3, 3, 4)
+    plt.imshow(
+        np.transpose(y[index, 1, :, :]),
+        cmap="jet",
+        vmin=min_v,
+        vmax=max_v,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.ylabel("Uy", fontsize=18)
+    plt.subplot(3, 3, 5)
+    plt.imshow(
+        np.transpose(pred_y[index, 1, :, :]),
+        cmap="jet",
+        vmin=min_v,
+        vmax=max_v,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.subplot(3, 3, 6)
+    plt.imshow(
+        np.transpose(error[index, 1, :, :]),
+        cmap="jet",
+        vmin=min_error_v,
+        vmax=max_error_v,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+
+    plt.subplot(3, 3, 7)
+    plt.imshow(
+        np.transpose(y[index, 2, :, :]),
+        cmap="jet",
+        vmin=min_p,
+        vmax=max_p,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.ylabel("p", fontsize=18)
+    plt.subplot(3, 3, 8)
+    plt.imshow(
+        np.transpose(pred_y[index, 2, :, :]),
+        cmap="jet",
+        vmin=min_p,
+        vmax=max_p,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.subplot(3, 3, 9)
+    plt.imshow(
+        np.transpose(error[index, 2, :, :]),
+        cmap="jet",
+        vmin=min_error_p,
+        vmax=max_error_p,
+        origin="lower",
+        extent=[0, 260, 0, 120],
+    )
+    plt.colorbar(orientation="horizontal")
+    plt.tight_layout()
+    plt.show()
+    plt.savefig(
+        os.path.join(plot_dir, f"cfd_{index}.png"),
+        bbox_inches="tight",
+    )
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", os.path.join(cfg.output_dir, "train.log"), "info")
+
+    # initialize datasets
+    with open(cfg.DATAX_PATH, "rb") as file:
+        x = pickle.load(file)
+    with open(cfg.DATAY_PATH, "rb") as file:
+        y = pickle.load(file)
+
+    # split dataset to train dataset and test dataset
+    train_dataset, test_dataset = split_tensors(x, y, ratio=cfg.SLIPT_RATIO)
+    train_x, train_y = train_dataset
+    test_x, test_y = test_dataset
+
+    # initialize model
+    model = ppsci.arch.UNetEx(**cfg.MODEL)
+
+    CHANNELS_WEIGHTS = np.reshape(
+        np.sqrt(
+            np.mean(
+                np.transpose(y, (0, 2, 3, 1)).reshape(
+                    (cfg.SAMPLE_SIZE * cfg.X_SIZE * cfg.Y_SIZE, cfg.CHANNEL_SIZE)
+                )
+                ** 2,
+                axis=0,
+            )
+        ),
+        (1, -1, 1, 1),
+    )
+
+    # define loss
+    def loss_expr(
+        output_dict: Dict[str, np.ndarray],
+        label_dict: Dict[str, np.ndarray] = None,
+        weight_dict: Dict[str, np.ndarray] = None,
+    ) -> float:
+        output = output_dict["output"]
+        y = label_dict["output"]
+        loss_u = (output[:, 0:1, :, :] - y[:, 0:1, :, :]) ** 2
+        loss_v = (output[:, 1:2, :, :] - y[:, 1:2, :, :]) ** 2
+        loss_p = (output[:, 2:3, :, :] - y[:, 2:3, :, :]).abs()
+        loss = (loss_u + loss_v + loss_p) / CHANNELS_WEIGHTS
+        return {"output": loss.sum()}
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {"input": train_x},
+                "label": {"output": train_y},
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.FunctionalLoss(loss_expr),
+        name="sup_constraint",
+    )
+
+    # manually build constraint
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # initialize Adam optimizer
+    optimizer = ppsci.optimizer.Adam(
+        cfg.TRAIN.learning_rate, weight_decay=cfg.TRAIN.weight_decay
+    )(model)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"input": test_x},
+            "label": {"output": test_y},
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    def metric_expr(
+        output_dict: Dict[str, np.ndarray],
+        label_dict: Dict[str, np.ndarray] = None,
+        weight_dict: Dict[str, np.ndarray] = None,
+    ) -> Dict[str, float]:
+        output = output_dict["output"]
+        y = label_dict["output"]
+        total_mse = ((output - y) ** 2).sum() / len(test_x)
+        ux_mse = ((output[:, 0, :, :] - test_y[:, 0, :, :]) ** 2).sum() / len(test_x)
+        uy_mse = ((output[:, 1, :, :] - test_y[:, 1, :, :]) ** 2).sum() / len(test_x)
+        p_mse = ((output[:, 2, :, :] - test_y[:, 2, :, :]) ** 2).sum() / len(test_x)
+        return {
+            "Total_MSE": total_mse,
+            "Ux_MSE": ux_mse,
+            "Uy_MSE": uy_mse,
+            "p_MSE": p_mse,
+        }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.FunctionalLoss(loss_expr),
+        {"output": lambda out: out["output"]},
+        {"MSE": ppsci.metric.FunctionalMetric(metric_expr)},
+        name="mse_validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        seed=cfg.seed,
+        validator=validator,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # train model
+    solver.train()
+
+    # evaluate after finished training
+    solver.eval()
+
+    PLOT_DIR = os.path.join(cfg.output_dir, "visual")
+    os.makedirs(PLOT_DIR, exist_ok=True)
+
+    # visualize prediction after finished training
+    predict_and_save_plot(test_x, test_y, 0, solver, PLOT_DIR)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", os.path.join(cfg.output_dir, "eval.log"), "info")
+
+    # initialize datasets
+    with open(cfg.DATAX_PATH, "rb") as file:
+        x = pickle.load(file)
+    with open(cfg.DATAY_PATH, "rb") as file:
+        y = pickle.load(file)
+
+    # split dataset to train dataset and test dataset
+    train_dataset, test_dataset = split_tensors(x, y, ratio=cfg.SLIPT_RATIO)
+    train_x, train_y = train_dataset
+    test_x, test_y = test_dataset
+
+    # initialize model
+    model = ppsci.arch.UNetEx(**cfg.MODEL)
+
+    CHANNELS_WEIGHTS = np.reshape(
+        np.sqrt(
+            np.mean(
+                np.transpose(y, (0, 2, 3, 1)).reshape(
+                    (cfg.SAMPLE_SIZE * cfg.X_SIZE * cfg.Y_SIZE, cfg.CHANNEL_SIZE)
+                )
+                ** 2,
+                axis=0,
+            )
+        ),
+        (1, -1, 1, 1),
+    )
+
+    # define loss
+    def loss_expr(
+        output_dict: Dict[str, np.ndarray],
+        label_dict: Dict[str, np.ndarray] = None,
+        weight_dict: Dict[str, np.ndarray] = None,
+    ) -> float:
+        output = output_dict["output"]
+        y = label_dict["output"]
+        loss_u = (output[:, 0:1, :, :] - y[:, 0:1, :, :]) ** 2
+        loss_v = (output[:, 1:2, :, :] - y[:, 1:2, :, :]) ** 2
+        loss_p = (output[:, 2:3, :, :] - y[:, 2:3, :, :]).abs()
+        loss = (loss_u + loss_v + loss_p) / CHANNELS_WEIGHTS
+        return loss.sum()
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"input": test_x},
+            "label": {"output": test_y},
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    def metric_expr(
+        output_dict: Dict[str, np.ndarray],
+        label_dict: Dict[str, np.ndarray] = None,
+        weight_dict: Dict[str, np.ndarray] = None,
+    ) -> Dict[str, float]:
+        output = output_dict["output"]
+        y = label_dict["output"]
+        total_mse = ((output - y) ** 2).sum() / len(test_x)
+        ux_mse = ((output[:, 0, :, :] - test_y[:, 0, :, :]) ** 2).sum() / len(test_x)
+        uy_mse = ((output[:, 1, :, :] - test_y[:, 1, :, :]) ** 2).sum() / len(test_x)
+        p_mse = ((output[:, 2, :, :] - test_y[:, 2, :, :]) ** 2).sum() / len(test_x)
+        return {
+            "Total_MSE": total_mse,
+            "Ux_MSE": ux_mse,
+            "Uy_MSE": uy_mse,
+            "p_MSE": p_mse,
+        }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.FunctionalLoss(loss_expr),
+        {"output": lambda out: out["output"]},
+        {"MSE": ppsci.metric.FunctionalMetric(metric_expr)},
+        name="mse_validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # evaluate
+    solver.eval()
+
+    PLOT_DIR = os.path.join(cfg.output_dir, "visual")
+    os.makedirs(PLOT_DIR, exist_ok=True)
+
+    # visualize prediction
+    predict_and_save_plot(test_x, test_y, 0, solver, PLOT_DIR)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="deepcfd.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+
+

DeepCFD +

+
OpenFOAM 计算结果与 DeepCFD 预测结果对比,从上到下分别为:水平速度分量(Ux),垂直速度分量(Uy)以及流体压强(p)
+
+

可以看到DeepCFD方法与OpenFOAM的结果基本一致。

+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/deephpms/index.html b/zh/examples/deephpms/index.html new file mode 100644 index 0000000000..54ae00c95b --- /dev/null +++ b/zh/examples/deephpms/index.html @@ -0,0 +1,5633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DeepHPMs - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

DeepHPMs(Deep Hidden Physics Models)

+

AI Studio快速体验

+
+
+
+
# 案例 1
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+python burgers.py DATASET_PATH=./datasets/burgers_sine.mat DATASET_PATH_SOL=./datasets/burgers_sine.mat
+
+# 案例 2
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat --create-dirs -o ./datasets/burgers.mat
+python burgers.py DATASET_PATH=./datasets/burgers_sine.mat DATASET_PATH_SOL=./datasets/burgers.mat
+
+# 案例 3
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat --create-dirs -o ./datasets/burgers.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+python burgers.py DATASET_PATH=./datasets/burgers.mat DATASET_PATH_SOL=./datasets/burgers_sine.mat
+
+# 案例 4
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat --create-dirs -o ./datasets/KdV_sine.mat
+python korteweg_de_vries.py DATASET_PATH=./datasets/KdV_sine.mat DATASET_PATH_SOL=./datasets/KdV_sine.mat
+
+# 案例 5
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_cos.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat --create-dirs -o ./datasets/KdV_sine.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_cos.mat --create-dirs -o ./datasets/KdV_cos.mat
+python korteweg_de_vries.py DATASET_PATH=./datasets/KdV_sine.mat DATASET_PATH_SOL=./datasets/KdV_cos.mat
+
+# 案例 6
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KS.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KS.mat --create-dirs -o ./datasets/KS.mat
+python kuramoto_sivashinsky.py DATASET_PATH=./datasets/KS.mat DATASET_PATH_SOL=./datasets/KS.mat
+
+# 案例 7
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/cylinder.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/cylinder.mat --create-dirs -o ./datasets/cylinder.mat
+python navier_stokes.py DATASET_PATH=./datasets/cylinder.mat DATASET_PATH_SOL=./datasets/cylinder.mat
+
+# 案例 8
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/NLS.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/NLS.mat --create-dirs -o ./datasets/NLS.mat
+python schrodinger.py DATASET_PATH=./datasets/NLS.mat DATASET_PATH_SOL=./datasets/NLS.mat
+
+
+
+
# 案例 1
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+python burgers.py mode=eval DATASET_PATH=./datasets/burgers_sine.mat DATASET_PATH_SOL=./datasets/burgers_sine.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/burgers_same_pretrained.pdparams
+
+# 案例 2
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat --create-dirs -o ./datasets/burgers.mat
+python burgers.py mode=eval DATASET_PATH=./datasets/burgers_sine.mat DATASET_PATH_SOL=./datasets/burgers.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/burgers_diff_pretrained.pdparams
+
+# 案例 3
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers.mat --create-dirs -o ./datasets/burgers.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/burgers_sine.mat --create-dirs -o ./datasets/burgers_sine.mat
+python burgers.py mode=eval DATASET_PATH=./datasets/burgers.mat DATASET_PATH_SOL=./datasets/burgers_sine.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/burgers_diff_swap_pretrained.pdparams
+
+# 案例 4
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat --create-dirs -o ./datasets/KdV_sine.mat
+python korteweg_de_vries.py mode=eval DATASET_PATH=./datasets/KdV_sine.mat DATASET_PATH_SOL=./datasets/KdV_sine.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/kdv_same_pretrained.pdparams
+
+# 案例 5
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_cos.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_sine.mat --create-dirs -o ./datasets/KdV_sine.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KdV_cos.mat --create-dirs -o ./datasets/KdV_cos.mat
+python korteweg_de_vries.py mode=eval DATASET_PATH=./datasets/KdV_sine.mat DATASET_PATH_SOL=./datasets/KdV_cos.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/kdv_diff_pretrained.pdparams
+
+# 案例 6
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KS.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/KS.mat --create-dirs -o ./datasets/KS.mat
+python kuramoto_sivashinsky.py mode=eval DATASET_PATH=./datasets/KS.mat DATASET_PATH_SOL=./datasets/KS.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/ks_pretrained.pdparams
+
+# 案例 7
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/cylinder.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/cylinder.mat --create-dirs -o ./datasets/cylinder.mat
+python navier_stokes.py mode=eval DATASET_PATH=./datasets/cylinder.mat DATASET_PATH_SOL=./datasets/cylinder.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/ns_pretrained.pdparams
+
+# 案例 8
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/NLS.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepHPMs/NLS.mat --create-dirs -o ./datasets/NLS.mat
+python schrodinger.py mode=eval DATASET_PATH=./datasets/NLS.mat DATASET_PATH_SOL=./datasets/NLS.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/DeepHPMs/schrodinger_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
序号案例名称stage1、2 数据集stage3(eval)数据集预训练模型指标
1burgersburgers_sine.matburgers_sine.matburgers_same_pretrained.pdparamsl2 error: 0.0088
2burgersburgers_sine.matburgers.matburgers_diff_pretrained.pdparamsl2 error: 0.0379
3burgersburgers.matburgers_sine.matburgers_diff_swap_pretrained.pdparamsl2 error: 0.2904
4korteweg_de_vriesKdV_sine.matKdV_sine.matkdv_same_pretrained.pdparamsl2 error: 0.0567
5korteweg_de_vriesKdV_sine.matKdV_cos.matkdv_diff_pretrained.pdparamsl2 error: 0.1142
6kuramoto_sivashinskyKS.matKS.matks_pretrained.pdparamsl2 error: 0.1166
7navier_stokescylinder.matcylinder.matns_pretrained.pdparamsl2 error: 0.0288
8schrodingerNLS.matNLS.matschrodinger_pretrained.pdparamsl2 error: 0.0735
+

注:根据 参考文献, 序号 3 的效果较差。

+

1. 背景简介

+

求解偏微分方程(PDE) 是一类基础的物理问题,在过去几十年里,以有限差分(FDM)、有限体积(FVM)、有限元(FEM)为代表的多种偏微分方程组数值解法趋于成熟。随着人工智能技术的高速发展,利用深度学习求解偏微分方程成为新的研究趋势。PINNs(Physics-informed neural networks) 是一种加入物理约束的深度学习网络,因此与纯数据驱动的神经网络学习相比,PINNs 可以用更少的数据样本学习到更具泛化能力的模型,其应用范围包括但不限于流体力学、热传导、电磁场、量子力学等领域。

+

传统的 PINNs 会将 PDE 作为 loss 的一项参与到网络训练中去,这就要求 PDE 公式为已知的先验条件,当 PDE 公式未知时,这种方法就不能实现。

+

DeepHPMs 着眼于 PDE 公式未知的情况,通过深度学习网络,从实验产生的高维数据中发现物理规律,即非线性 PDE 方程,并用一个深度学习网络来表征这个 PDE 方程,再将这个 PDE 网络替代传统 PINNs 方法中的 PDE 公式,对新的数据进行预测。

+

本问题对 Burgers, Korteweg- de Vries (KdV), Kuramoto-Sivashinsky, nonlinear Schro ̈dinger 和 Navier- Stokes equations 多种 PDE 方程进行了研究,本文档主要针对 Burgers 方程进行说明。

+

2. 问题定义

+

伯格斯方程(Burgers equation) 是一个模拟冲击波的传播和反射的非线性偏微分方程,该方程认为输出的解 \(u\) 与输入的位置、时间参数 \((x, t)\) 之间的关系为:

+
\[ +u_t + \lambda_1 u u_x - \lambda_2 u_{xx} = 0 +\]
+

其中 \(u_t\)\(u\)\(t\) 的偏导数,\(u_x\)\(u\)\(x\) 的偏导数,\(u_{xx}\)\(u\)\(x\) 的二阶偏导数。

+

通过深度学习网络表示 PDE,即 \(u_t\) 是输入为 \(u, u_x, u_{xx}\) 的网络的输出:

+
\[ +u_t = \mathcal{N}(u, u_x, u_{xx}) +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集为处理好的 burgers 数据集,包含不同初始化条件下模拟数据的 \(x, t, u\) 以字典的形式存储在 .mat 文件中。

+

运行本问题代码前请下载 模拟数据集1模拟数据集2, 下载后分别存放在路径:

+
DATASET_PATH: ./datasets/burgers_sine.mat
+DATASET_PATH_SOL: ./datasets/burgers_sine.mat
+
+

3.2 模型构建

+

本问题共包含 3 个深度学习网络,分别为数据驱动的 Net1,表征 PDE 方程的 Net2 以及用于推理新数据的 Net3。

+

Net1 通过数据驱动的方式,使用输入的某种模拟情况 1 下的少量随机数据进行训练,学习数据规律,从而得到该模拟情况下其他所有数据的数值 \(u\)。输入为模拟情况 1 数据的 \(x, t\),输出为 \(u\),是一个\((x, t)\)\(u\) 的映射函数 \(f_1: \mathbb{R}^2 \to \mathbb{R}^1\)

+

对 Net1 前向推理得到的 \(u\) 值,计算其对 \(x, t\) 的偏导数 \(u_t, u_x, u_{xx}\),并将计算得到的值当作真实物理值,作为输入和标签传到 Net2 中,通过优化 loss,训练 Net2。对于 Net2,输入为 Net1 推理得到的 \(u\) 以及它对x的偏导数 \(u_x, u_{xx}\),输出为 PDE 的运算结果 \(f_{pde}\),这个值应该与 \(u_t\) 接近,即 \(u_t\)\(f_{pde}\) 的 label。映射函数为 \(f_2: \mathbb{R}^3 \to \mathbb{R}^1\)

+

最后,将训练好的 Net2 当作 PDE 公式,将新的模拟情况 2 下的少量数据作为输入,与 Net3 一起进行类似 PINNs 的训练,最终得到可以对模拟情况 2 进行预测的深度学习网络 Net3。对于 Net3,输入为模拟情况 2 数据的 \(x, t\),输出为 \(u\),是一个\((x, t)\)\(u\) 的映射函数 \(f_3: \mathbb{R}^2 \to \mathbb{R}^1\)

+

因为训练中后一个阶段网络需要使用前一个阶段网络的前向推理值,因此本问题使用 Model List 来实现,上式中 \(f_1,f_2,f_3\) 分别为一个 MLP 模型,三者共同构成了一个 Model List,用 PaddleScience 代码表示如下

+
# initialize model list
+model_list = ppsci.arch.ModelList((model_idn, model_pde, model_sol))
+
+

注意到部分网络的输入由之前的网络计算得到,而不仅仅是数据中 \((x, t)\) 这两个变量,这也就意味着我们需要对部分网络输入进行 transform。

+

3.3 transform构建

+

对于 Net1,输入为 \((x, t)\) 本来不需要 transform,但由于训练中根据数据的定义域对输入数据进行了数值变换,因此同样需要transform,同样,Net3 也需要对输入进行数值变换的 transform

+
76
+77
+78
+79
+80
+81
def transform_u(_in):
+    t, x = _in["t"], _in["x"]
+    t = 2.0 * (t - t_lb) * paddle.pow((t_ub - t_lb), -1) - 1.0
+    x = 2.0 * (x - x_lb) * paddle.pow((x_ub - x_lb), -1) - 1.0
+    input_trans = {"t": t, "x": x}
+    return input_trans
+
+

对于 Net2,因为它的输入为 \(u, u_x, u_{xx}\)\(u\) 为其他两个网络的输出,只要进行 Net2 的前向推理,就需要 transform,因此需要两种 transform。同时,在训练 Net3 之前,需要重新注册 transform

+
83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
def transform_f(input, model, out_key):
+    in_idn = {"t": input["t"], "x": input["x"]}
+    x = input["x"]
+    u = model(in_idn)[out_key]
+    du_x = jacobian(u, x)
+    du_xx = hessian(u, x)
+    input_trans = {"u_x": u, "du_x": du_x, "du_xx": du_xx}
+    return input_trans
+
+def transform_f_idn(_in):
+    return transform_f(_in, model_idn, "u_idn")
+
+

然后依次注册 transform 后,将3 个 MLP 模型组成 Model List

+
# register transform
+model_idn.register_input_transform(transform_u)
+model_pde.register_input_transform(transform_f_idn)
+model_sol.register_input_transform(transform_u)
+
+# initialize model list
+model_list = ppsci.arch.ModelList((model_idn, model_pde, model_sol))
+
+

注意 Net3 开始训练前,重新注册 Net2 的transform

+
# re-register transform for model 2, fit for loss of stage 3
+model_pde.register_input_transform(transform_f_sol)
+
+

这样我们就实例化出了一个拥有 3 个 MLP 模型,每个 MLP 包含 4 层隐藏神经元,每层神经元数为 50,使用 "sin" 作为激活函数,并包含输入 transform 的神经网络模型 model list

+

3.4 参数和超参数设定

+

我们需要指定问题相关的参数,如数据集路径、输出文件路径、定义域的值等

+
26
+27
+28
+29
+30
+31
+32
+33
DATASET_PATH: ./datasets/burgers_sine.mat
+DATASET_PATH_SOL: ./datasets/burgers_sine.mat
+
+# set working condition
+T_LB: 0.0
+T_UB: 10.0
+X_LB: -8.0
+X_UB: 8.0
+
+

同时需要指定训练轮数和学习率等超参数

+
57
+58
+59
+60
+61
TRAIN:
+  epochs: 50000 # set 1 for LBFGS
+  iters_per_epoch: 1
+  max_iter: 50000  # for LBFGS
+  learning_rate: 1.0e-3
+
+

3.5 优化器构建

+

本问题提供了两种优化器,分别为 Adam 优化器和 LBFGS 优化器,训练时只选择其中一种,需要将另一种优化器注释掉。

+
# initialize optimizer
+# Adam
+optimizer_idn = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_idn)
+optimizer_pde = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_pde)
+optimizer_sol = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_sol)
+
+# LBFGS
+# optimizer_idn = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_idn)
+# optimizer_pde = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_pde)
+# optimizer_sol = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_sol)
+
+

3.6 约束构建

+

本问题分为三个训练阶段,部分采用监督学习的方式,对 \(u\) 进行约束,部分采用无监督学习的方式,约束结果满足 PDE 公式。

+

无监督仍然可以采用监督约束 SupervisedConstraint,在定义约束之前,需要给监督约束指定文件路径等数据读取配置,因为数据集中没有标签数据,因此在数据读取时我们需要使用训练数据充当标签数据,并注意在之后不要使用这部分“假的”标签数据,例如

+
train_dataloader_cfg_idn = {
+    "dataset": {
+        "name": "IterableMatDataset",
+        "file_path": cfg.DATASET_PATH,
+        "input_keys": ("t", "x"),
+        "label_keys": ("u_idn",),
+        "alias_dict": {"t": "t_train", "x": "x_train", "u_idn": "u_train"},
+    },
+}
+
+

du_t 值读取了 t 的值,是“假的”标签数据。

+

3.6.1 第一阶段约束构建

+

第一阶段对 Net1 的训练是纯监督学习,此处采用监督约束 SupervisedConstraint

+
sup_constraint_idn = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_idn,
+    ppsci.loss.MSELoss("sum"),
+    {"u_idn": lambda out: out["u_idn"]},
+    name="u_mse_sup",
+)
+constraint_idn = {sup_constraint_idn.name: sup_constraint_idn}
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,配置中 “dataset” 字段表示使用的训练数据集信息,其各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 "IterableMatDataset" 表示不分 batch 顺序读取的 .mat 类型的数据集;
  2. +
  3. file_path: 数据集文件路径;
  4. +
  5. input_keys: 输入变量名;
  6. +
  7. label_keys: 标签变量名;
  8. +
  9. alias_dict: 变量别名。
  10. +
+

第二个参数是损失函数,由于是纯数据驱动,此处使用 MSE

+

第三个参数是方程表达式,用于描述如何计算约束目标,计算后的值将会按照指定名称存入输出列表中,从而保证 loss 计算时可以使用这些值。

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+

3.6.2 第二阶段约束构建

+

第二阶段对 Net2 的训练是无监督学习,但仍可采用监督约束 SupervisedConstraint,要注意上述提到的给定“假的”标签数据

+
sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_pde,
+    ppsci.loss.FunctionalLoss(pde_loss_func),
+    {
+        "du_t": lambda out: jacobian(out["u_idn"], out["t"]),
+        "f_pde": lambda out: out["f_pde"],
+    },
+    name="f_mse_sup",
+)
+constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+

各个参数含义与 第一阶段约束构建 一致,唯一的区别是这个约束中的第二个参数,损失函数,采用 PaddleScience 预留的自定义 loss 函数类 FunctionalLoss,该类支持编写代码时自定义 loss 的计算方法,而不是使用诸如 MSE 等现有方法。本约束中的自定义 loss 函数代码请参考 自定义 loss 和 metric

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+

3.6.3 第三阶段约束构建

+

第三阶段 Net3 的训练复杂,包含了对部分初始点的监督学习、与 PDE 有关的无监督学习以及与边界条件有关的无监督学习,这里仍采用监督约束 SupervisedConstraint,同样要注意给定“假的”标签数据,各参数含义同上

+
sup_constraint_sol_f = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_sol_f,
+    ppsci.loss.FunctionalLoss(pde_loss_func),
+    {
+        "f_pde": lambda out: out["f_pde"],
+        "du_t": lambda out: jacobian(out["u_sol"], out["t"]),
+    },
+    name="f_mse_sup",
+)
+sup_constraint_sol_init = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_sol_init,
+    ppsci.loss.MSELoss("sum"),
+    {"u_sol": lambda out: out["u_sol"]},
+    name="u0_mse_sup",
+)
+sup_constraint_sol_bc = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_sol_bc,
+    ppsci.loss.FunctionalLoss(boundary_loss_func),
+    {
+        "x": lambda out: out["x"],
+        "u_sol": lambda out: out["u_sol"],
+    },
+    name="ub_mse_sup",
+)
+constraint_sol = {
+    sup_constraint_sol_f.name: sup_constraint_sol_f,
+    sup_constraint_sol_init.name: sup_constraint_sol_init,
+    sup_constraint_sol_bc.name: sup_constraint_sol_bc,
+}
+
+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+

3.7 评估器构建

+

与约束同理,虽然本问题部分采用监督学习的方式,部分采用无监督学习的方式,但仍可以使用 ppsci.validate.SupervisedValidator 构建评估器,参数含义也与约束构建,唯一的区别是评价指标 metric

+

3.7.1 第一阶段评估器构建

+

评价指标 metricL2 正则化函数

+
sup_validator_idn = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg_idn,
+    ppsci.loss.MSELoss("sum"),
+    {"u_idn": lambda out: out["u_idn"]},
+    {"l2": ppsci.metric.L2Rel()},
+    name="u_L2_sup",
+)
+validator_idn = {sup_validator_idn.name: sup_validator_idn}
+
+

3.7.2 第二阶段评估器构建

+

评价指标 metricFunctionalMetric,这是 PaddleScience 预留的自定义 metric 函数类,该类支持编写代码时自定义 metric 的计算方法,而不是使用诸如 MSEL2 等现有方法。自定义 metric 函数代码请参考下一部分 自定义 loss 和 metric

+
sup_validator_pde = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg_pde,
+    ppsci.loss.FunctionalLoss(pde_loss_func),
+    {
+        "du_t": lambda out: jacobian(out["u_idn"], out["t"]),
+        "f_pde": lambda out: out["f_pde"],
+    },
+    {"l2": ppsci.metric.FunctionalMetric(pde_l2_rel_func)},
+    name="f_L2_sup",
+)
+validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+

3.7.3 第三阶段评估器构建

+

因为第三阶段评价时只需要对训练得到的点的值进行评价,而不需要对边界条件满足程度或 PDE 满足程度进行评价,因此评价指标 metricL2 正则化函数

+
sup_validator_sol = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg_sol,
+    ppsci.loss.MSELoss("sum"),
+    {"u_sol": lambda out: out["u_sol"]},
+    {"l2": ppsci.metric.L2Rel()},
+    name="u_L2_sup",
+)
+validator_sol = {sup_validator_sol.name: sup_validator_sol}
+
+

3.8 自定义 loss 和 metric

+

由于本问题包含无监督学习,数据中不存在标签数据,loss 和 metric 根据 PDE 计算得到,因此需要自定义 loss 和 metric。方法为先定义相关函数,再将函数名作为参数传给 FunctionalLossFunctionalMetric

+

需要注意自定义 loss 和 metric 函数的输入输出参数需要与 PaddleScience 中如 MSE 等其他函数保持一致,即输入为模型输出 output_dict 等字典变量,loss 函数输出为 loss 值 paddle.Tensor,metric 函数输出为字典 Dict[str, paddle.Tensor]

+

与 PDE 相关的自定义 loss 函数为

+
32
+33
+34
def pde_loss_func(output_dict, *args):
+    losses = F.mse_loss(output_dict["f_pde"], output_dict["du_t"], "sum")
+    return {"pde": losses}
+
+

与 PDE 相关的自定义 metric 函数为

+
37
+38
+39
+40
+41
+42
def pde_l2_rel_func(output_dict, *args):
+    rel_l2 = paddle.norm(output_dict["du_t"] - output_dict["f_pde"]) / paddle.norm(
+        output_dict["du_t"]
+    )
+    metric_dict = {"f_pde": rel_l2}
+    return metric_dict
+
+

与边界条件相关的自定义 loss 函数为

+
45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
def boundary_loss_func(output_dict, *args):
+    u_b = output_dict["u_sol"]
+    u_lb, u_ub = paddle.split(u_b, 2, axis=0)
+
+    x_b = output_dict["x"]
+    du_x = jacobian(u_b, x_b)
+
+    du_x_lb, du_x_ub = paddle.split(du_x, 2, axis=0)
+
+    losses = F.mse_loss(u_lb, u_ub, "sum")
+    losses += F.mse_loss(du_x_lb, du_x_ub, "sum")
+    return {"boundary": losses}
+
+

3.9 模型训练、评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给各个阶段 ppsci.solver.Solver,然后启动训练、评估。

+

第一阶段训练、评估

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model_list,
+    constraint_idn,
+    cfg.output_dir,
+    optimizer_idn,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    validator=validator_idn,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

第二阶段训练、评估

+
# update solver
+solver = ppsci.solver.Solver(
+    model_list,
+    constraint_pde,
+    cfg.output_dir,
+    optimizer_pde,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    validator=validator_pde,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

第三阶段训练、评估

+
# update solver
+solver = ppsci.solver.Solver(
+    model_list,
+    constraint_sol,
+    cfg.output_dir,
+    optimizer_sol,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    validator=validator_sol,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.10 可视化

+

本问题训练结束后,可以在 evalution 中使用第三阶段网络 Net3 对模拟情况 2 的数据进行推理,结果为 \(u|_{(x,t)}\) 值,同时输出 l2 error 的值。画图部分在 plotting.py 文件中。

+
# stage 3: solution net
+# load pretrained model
+save_load.load_pretrain(model_list, cfg.EVAL.pretrained_model_path)
+
+# load dataset
+dataset_val = reader.load_mat_file(
+    cfg.DATASET_PATH_SOL,
+    keys=("t", "x", "u_sol"),
+    alias_dict={
+        "t": "t_ori",
+        "x": "x_ori",
+        "u_sol": "Exact_ori",
+    },
+)
+
+t_sol, x_sol = np.meshgrid(
+    np.squeeze(dataset_val["t"]), np.squeeze(dataset_val["x"])
+)
+t_sol_flatten = paddle.to_tensor(
+    t_sol.flatten()[:, None], dtype=paddle.get_default_dtype(), stop_gradient=False
+)
+x_sol_flatten = paddle.to_tensor(
+    x_sol.flatten()[:, None], dtype=paddle.get_default_dtype(), stop_gradient=False
+)
+u_sol_pred = model_list({"t": t_sol_flatten, "x": x_sol_flatten})
+
+# eval
+l2_error = np.linalg.norm(
+    dataset_val["u_sol"] - u_sol_pred["u_sol"], 2
+) / np.linalg.norm(dataset_val["u_sol"], 2)
+logger.info(f"l2_error: {l2_error}")
+
+# plotting
+plot_points = paddle.concat([t_sol_flatten, x_sol_flatten], axis=-1).numpy()
+plot_func.draw_and_save(
+    figname="burgers_sol",
+    data_exact=dataset_val["u_sol"],
+    data_learned=u_sol_pred["u_sol"].numpy(),
+    boundary=[cfg.T_LB, cfg.T_UB, cfg.X_LB, cfg.X_UB],
+    griddata_points=plot_points,
+    griddata_xi=(t_sol, x_sol),
+    save_path=cfg.output_dir,
+)
+
+

4. 完整代码

+
burgers.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+import paddle.nn.functional as F
+import plotting as plot_func
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import hessian
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+from ppsci.utils import reader
+from ppsci.utils import save_load
+
+
+def pde_loss_func(output_dict, *args):
+    losses = F.mse_loss(output_dict["f_pde"], output_dict["du_t"], "sum")
+    return {"pde": losses}
+
+
+def pde_l2_rel_func(output_dict, *args):
+    rel_l2 = paddle.norm(output_dict["du_t"] - output_dict["f_pde"]) / paddle.norm(
+        output_dict["du_t"]
+    )
+    metric_dict = {"f_pde": rel_l2}
+    return metric_dict
+
+
+def boundary_loss_func(output_dict, *args):
+    u_b = output_dict["u_sol"]
+    u_lb, u_ub = paddle.split(u_b, 2, axis=0)
+
+    x_b = output_dict["x"]
+    du_x = jacobian(u_b, x_b)
+
+    du_x_lb, du_x_ub = paddle.split(du_x, 2, axis=0)
+
+    losses = F.mse_loss(u_lb, u_ub, "sum")
+    losses += F.mse_loss(du_x_lb, du_x_ub, "sum")
+    return {"boundary": losses}
+
+
+def train(cfg: DictConfig):
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # initialize burgers boundaries
+    t_lb = paddle.to_tensor(cfg.T_LB)
+    t_ub = paddle.to_tensor(cfg.T_UB)
+    x_lb = paddle.to_tensor(cfg.X_LB)
+    x_ub = paddle.to_tensor(cfg.T_UB)
+
+    # initialize models
+    model_idn = ppsci.arch.MLP(**cfg.MODEL.idn_net)
+    model_pde = ppsci.arch.MLP(**cfg.MODEL.pde_net)
+    model_sol = ppsci.arch.MLP(**cfg.MODEL.sol_net)
+
+    # initialize transform
+    def transform_u(_in):
+        t, x = _in["t"], _in["x"]
+        t = 2.0 * (t - t_lb) * paddle.pow((t_ub - t_lb), -1) - 1.0
+        x = 2.0 * (x - x_lb) * paddle.pow((x_ub - x_lb), -1) - 1.0
+        input_trans = {"t": t, "x": x}
+        return input_trans
+
+    def transform_f(input, model, out_key):
+        in_idn = {"t": input["t"], "x": input["x"]}
+        x = input["x"]
+        u = model(in_idn)[out_key]
+        du_x = jacobian(u, x)
+        du_xx = hessian(u, x)
+        input_trans = {"u_x": u, "du_x": du_x, "du_xx": du_xx}
+        return input_trans
+
+    def transform_f_idn(_in):
+        return transform_f(_in, model_idn, "u_idn")
+
+    def transform_f_sol(_in):
+        return transform_f(_in, model_sol, "u_sol")
+
+    # register transform
+    model_idn.register_input_transform(transform_u)
+    model_pde.register_input_transform(transform_f_idn)
+    model_sol.register_input_transform(transform_u)
+
+    # initialize model list
+    model_list = ppsci.arch.ModelList((model_idn, model_pde, model_sol))
+
+    # initialize optimizer
+    # Adam
+    optimizer_idn = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_idn)
+    optimizer_pde = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_pde)
+    optimizer_sol = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model_sol)
+
+    # LBFGS
+    # optimizer_idn = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_idn)
+    # optimizer_pde = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_pde)
+    # optimizer_sol = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(model_sol)
+
+    # stage 1: training identification net
+    # manually build constraint(s)
+    train_dataloader_cfg_idn = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("t", "x"),
+            "label_keys": ("u_idn",),
+            "alias_dict": {"t": "t_train", "x": "x_train", "u_idn": "u_train"},
+        },
+    }
+
+    sup_constraint_idn = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_idn,
+        ppsci.loss.MSELoss("sum"),
+        {"u_idn": lambda out: out["u_idn"]},
+        name="u_mse_sup",
+    )
+    constraint_idn = {sup_constraint_idn.name: sup_constraint_idn}
+
+    # manually build validator
+    eval_dataloader_cfg_idn = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("t", "x"),
+            "label_keys": ("u_idn",),
+            "alias_dict": {"t": "t_star", "x": "x_star", "u_idn": "u_star"},
+        },
+    }
+
+    sup_validator_idn = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg_idn,
+        ppsci.loss.MSELoss("sum"),
+        {"u_idn": lambda out: out["u_idn"]},
+        {"l2": ppsci.metric.L2Rel()},
+        name="u_L2_sup",
+    )
+    validator_idn = {sup_validator_idn.name: sup_validator_idn}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        constraint_idn,
+        cfg.output_dir,
+        optimizer_idn,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        validator=validator_idn,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    # stage 2: training pde net
+    # manually build constraint(s)
+    train_dataloader_cfg_pde = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("t", "x"),
+            "label_keys": ("du_t",),
+            "alias_dict": {"t": "t_train", "x": "x_train", "du_t": "t_train"},
+        },
+    }
+
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_pde,
+        ppsci.loss.FunctionalLoss(pde_loss_func),
+        {
+            "du_t": lambda out: jacobian(out["u_idn"], out["t"]),
+            "f_pde": lambda out: out["f_pde"],
+        },
+        name="f_mse_sup",
+    )
+    constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+    # manually build validator
+    eval_dataloader_cfg_pde = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("t", "x"),
+            "label_keys": ("du_t",),
+            "alias_dict": {"t": "t_star", "x": "x_star", "du_t": "t_star"},
+        },
+    }
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg_pde,
+        ppsci.loss.FunctionalLoss(pde_loss_func),
+        {
+            "du_t": lambda out: jacobian(out["u_idn"], out["t"]),
+            "f_pde": lambda out: out["f_pde"],
+        },
+        {"l2": ppsci.metric.FunctionalMetric(pde_l2_rel_func)},
+        name="f_L2_sup",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # update solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        constraint_pde,
+        cfg.output_dir,
+        optimizer_pde,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        validator=validator_pde,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    # stage 3: training solution net
+    # re-register transform for model 2, fit for loss of stage 3
+    model_pde.register_input_transform(transform_f_sol)
+
+    # manually build constraint(s)
+    train_dataloader_cfg_sol_f = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_SOL,
+            "input_keys": ("t", "x"),
+            "label_keys": ("du_t",),
+            "alias_dict": {"t": "t_f_train", "x": "x_f_train", "du_t": "t_f_train"},
+        },
+    }
+    train_dataloader_cfg_sol_init = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_SOL,
+            "input_keys": ("t", "x"),
+            "label_keys": ("u_sol",),
+            "alias_dict": {"t": "t0", "x": "x0", "u_sol": "u0"},
+        },
+    }
+    train_dataloader_cfg_sol_bc = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_SOL,
+            "input_keys": ("t", "x"),
+            "label_keys": ("x",),
+            "alias_dict": {"t": "tb", "x": "xb"},
+        },
+    }
+
+    sup_constraint_sol_f = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_sol_f,
+        ppsci.loss.FunctionalLoss(pde_loss_func),
+        {
+            "f_pde": lambda out: out["f_pde"],
+            "du_t": lambda out: jacobian(out["u_sol"], out["t"]),
+        },
+        name="f_mse_sup",
+    )
+    sup_constraint_sol_init = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_sol_init,
+        ppsci.loss.MSELoss("sum"),
+        {"u_sol": lambda out: out["u_sol"]},
+        name="u0_mse_sup",
+    )
+    sup_constraint_sol_bc = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_sol_bc,
+        ppsci.loss.FunctionalLoss(boundary_loss_func),
+        {
+            "x": lambda out: out["x"],
+            "u_sol": lambda out: out["u_sol"],
+        },
+        name="ub_mse_sup",
+    )
+    constraint_sol = {
+        sup_constraint_sol_f.name: sup_constraint_sol_f,
+        sup_constraint_sol_init.name: sup_constraint_sol_init,
+        sup_constraint_sol_bc.name: sup_constraint_sol_bc,
+    }
+
+    # manually build validator
+    eval_dataloader_cfg_sol = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_SOL,
+            "input_keys": ("t", "x"),
+            "label_keys": ("u_sol",),
+            "alias_dict": {"t": "t_star", "x": "x_star", "u_sol": "u_star"},
+        },
+    }
+
+    sup_validator_sol = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg_sol,
+        ppsci.loss.MSELoss("sum"),
+        {"u_sol": lambda out: out["u_sol"]},
+        {"l2": ppsci.metric.L2Rel()},
+        name="u_L2_sup",
+    )
+    validator_sol = {sup_validator_sol.name: sup_validator_sol}
+
+    # update solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        constraint_sol,
+        cfg.output_dir,
+        optimizer_sol,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        validator=validator_sol,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # initialize burgers boundaries
+    t_lb = paddle.to_tensor(cfg.T_LB)
+    t_ub = paddle.to_tensor(cfg.T_UB)
+    x_lb = paddle.to_tensor(cfg.X_LB)
+    x_ub = paddle.to_tensor(cfg.T_UB)
+
+    # initialize models
+    model_idn = ppsci.arch.MLP(**cfg.MODEL.idn_net)
+    model_pde = ppsci.arch.MLP(**cfg.MODEL.pde_net)
+    model_sol = ppsci.arch.MLP(**cfg.MODEL.sol_net)
+
+    # initialize transform
+    def transform_u(_in):
+        t, x = _in["t"], _in["x"]
+        t = 2.0 * (t - t_lb) * paddle.pow((t_ub - t_lb), -1) - 1.0
+        x = 2.0 * (x - x_lb) * paddle.pow((x_ub - x_lb), -1) - 1.0
+        input_trans = {"t": t, "x": x}
+        return input_trans
+
+    def transform_f(input, model, out_key):
+        in_idn = {"t": input["t"], "x": input["x"]}
+        x = input["x"]
+        u = model(in_idn)[out_key]
+        du_x = jacobian(u, x)
+        du_xx = hessian(u, x)
+        input_trans = {"u_x": u, "du_x": du_x, "du_xx": du_xx}
+        return input_trans
+
+    def transform_f_sol(_in):
+        return transform_f(_in, model_sol, "u_sol")
+
+    # register transform
+    model_idn.register_input_transform(transform_u)
+    model_pde.register_input_transform(transform_f_sol)
+    model_sol.register_input_transform(transform_u)
+
+    # initialize model list
+    model_list = ppsci.arch.ModelList((model_idn, model_pde, model_sol))
+
+    # stage 3: solution net
+    # load pretrained model
+    save_load.load_pretrain(model_list, cfg.EVAL.pretrained_model_path)
+
+    # load dataset
+    dataset_val = reader.load_mat_file(
+        cfg.DATASET_PATH_SOL,
+        keys=("t", "x", "u_sol"),
+        alias_dict={
+            "t": "t_ori",
+            "x": "x_ori",
+            "u_sol": "Exact_ori",
+        },
+    )
+
+    t_sol, x_sol = np.meshgrid(
+        np.squeeze(dataset_val["t"]), np.squeeze(dataset_val["x"])
+    )
+    t_sol_flatten = paddle.to_tensor(
+        t_sol.flatten()[:, None], dtype=paddle.get_default_dtype(), stop_gradient=False
+    )
+    x_sol_flatten = paddle.to_tensor(
+        x_sol.flatten()[:, None], dtype=paddle.get_default_dtype(), stop_gradient=False
+    )
+    u_sol_pred = model_list({"t": t_sol_flatten, "x": x_sol_flatten})
+
+    # eval
+    l2_error = np.linalg.norm(
+        dataset_val["u_sol"] - u_sol_pred["u_sol"], 2
+    ) / np.linalg.norm(dataset_val["u_sol"], 2)
+    logger.info(f"l2_error: {l2_error}")
+
+    # plotting
+    plot_points = paddle.concat([t_sol_flatten, x_sol_flatten], axis=-1).numpy()
+    plot_func.draw_and_save(
+        figname="burgers_sol",
+        data_exact=dataset_val["u_sol"],
+        data_learned=u_sol_pred["u_sol"].numpy(),
+        boundary=[cfg.T_LB, cfg.T_UB, cfg.X_LB, cfg.X_UB],
+        griddata_points=plot_points,
+        griddata_xi=(t_sol, x_sol),
+        save_path=cfg.output_dir,
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="burgers.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
plotting.py
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
from os import path as osp
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+from scipy.interpolate import griddata
+
+
+def _draw_subplot(subfigname, figdata, fig, gs, cmap, boundary, loc):
+    ax = plt.subplot(gs[:, loc])
+    h = ax.imshow(
+        figdata,
+        interpolation="nearest",
+        cmap=cmap,
+        extent=boundary,  # [cfg.T_LB, cfg.T_UB, cfg.X_LB, cfg.X_UB]
+        origin="lower",
+        aspect="auto",
+    )
+    divider = make_axes_locatable(ax)
+    cax = divider.append_axes("right", size="5%", pad=0.05)
+
+    fig.colorbar(h, cax=cax)
+    ax.set_xlabel("$t$")
+    ax.set_ylabel("$x$")
+    ax.set_aspect("auto", "box")
+    ax.set_title(subfigname, fontsize=10)
+
+
+def draw_and_save(
+    figname, data_exact, data_learned, boundary, griddata_points, griddata_xi, save_path
+):
+    fig = plt.figure(figname, figsize=(10, 6))
+    gs = gridspec.GridSpec(1, 2)
+    gs.update(top=0.8, bottom=0.2, left=0.1, right=0.9, wspace=0.5)
+
+    # Exact p(t,x,y)
+    plot_data_label = griddata(
+        griddata_points, data_exact.flatten(), griddata_xi, method="cubic"
+    )
+    _draw_subplot("Exact Dynamics", plot_data_label, fig, gs, "jet", boundary, loc=0)
+    # Predicted p(t,x,y)
+    plot_data_pred = griddata(
+        griddata_points, data_learned.flatten(), griddata_xi, method="cubic"
+    )
+    _draw_subplot("Learned Dynamics", plot_data_pred, fig, gs, "jet", boundary, loc=1)
+
+    plt.savefig(osp.join(save_path, figname))
+    plt.close()
+
+
+def draw_and_save_ns(figname, data_exact, data_learned, grid_data, save_path):
+    snap = 120
+    nn = 200
+    lb_x, lb_y = grid_data[:, 0].min(), grid_data[:, 1].min()
+    ub_x, ub_y = grid_data[:, 0].max(), grid_data[:, 1].max()
+    x_plot = np.linspace(lb_x, ub_x, nn)
+    y_plot = np.linspace(lb_y, ub_y, nn)
+    X_plot, Y_plot = np.meshgrid(x_plot, y_plot)
+
+    fig = plt.figure(figname, figsize=(10, 6))
+    gs = gridspec.GridSpec(1, 2)
+    gs.update(top=0.8, bottom=0.2, left=0.1, right=0.9, wspace=0.5)
+    # Exact p(t,x,y)
+    plot_data_label = griddata(
+        grid_data,
+        data_exact[:, snap].flatten(),
+        (X_plot, Y_plot),
+        method="cubic",
+    )
+    _draw_subplot(
+        "Exact Dynamics",
+        plot_data_label,
+        fig,
+        gs,
+        "seismic",
+        [lb_x, lb_y, ub_x, ub_y],
+        loc=0,
+    )
+    # Predicted p(t,x,y)
+    plot_data_pred = griddata(
+        grid_data,
+        data_learned[:, snap].flatten(),
+        (X_plot, Y_plot),
+        method="cubic",
+    )
+    _draw_subplot(
+        "Learned Dynamics",
+        plot_data_pred,
+        fig,
+        gs,
+        "seismic",
+        [lb_x, lb_y, ub_x, ub_y],
+        loc=1,
+    )
+    plt.savefig(osp.join(save_path, figname))
+    plt.close()
+
+

5. 结果展示

+

参考 问题定义,下图横、纵坐标分别为时间、位置参数,颜色表示 burgers 的解 u,大小参照图片右侧的颜色卡。将 burgers 方程应用在不同的问题上,u 存在不同的含义,这里可以简单的认为 u 值代表速度。

+

下图展示了在一定初始条件(t=0 时刻 x 对应的 u 值)下,随 t 增长,u 随 x 变化 的情况。u 的真实值和模型预测结果如下,与传统的光谱方法相比基本一致。

+

模拟数据集 1 是初始条件为 sin 方程时的数据集 burgers_sine.mat,模拟数据集 2 是初始条件为 exp 方程时的数据集 burgers.mat:

+
+

burgers_diff_lbfgs +

+
真实 u 值和预测 u 值对比
+
+

模拟数据集 1、2 都是初始条件为 sin 方程时的数据集 burgers_sine.mat 时:

+
+

burgers_same_lbfgs +

+
真实 u 值和预测 u 值对比
+
+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/deeponet/index.html b/zh/examples/deeponet/index.html new file mode 100644 index 0000000000..4252ff4083 --- /dev/null +++ b/zh/examples/deeponet/index.html @@ -0,0 +1,4527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DeepONet - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

DeepONet

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepONet/antiderivative_unaligned_train.npz
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepONet/antiderivative_unaligned_test.npz
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/deeponet/antiderivative_unaligned_train.npz -o antiderivative_unaligned_train.npz
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/deeponet/antiderivative_unaligned_test.npz -o antiderivative_unaligned_test.npz
+python deeponet.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepONet/antiderivative_unaligned_train.npz
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/DeepONet/antiderivative_unaligned_test.npz
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/deeponet/antiderivative_unaligned_train.npz -o antiderivative_unaligned_train.npz
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/deeponet/antiderivative_unaligned_test.npz -o antiderivative_unaligned_test.npz
+python deeponet.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/deeponet/deeponet_pretrained.pdparams
+
+
+
+
python deeponet.py mode=export
+
+
+
+
python deeponet.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
deeponet_pretrained.pdparamsloss(G_eval): 0.00003
L2Rel.G(G_eval): 0.01799
+

1. 背景简介

+

根据机器学习领域的万能近似定理,一个神经网络模型不仅可以拟合输入数据到输出数据的函数映射关系,也可以扩展到对函数与函数之间的映射关系进行拟合,称之为“算子”学习。

+

因此 DeepONet 在各个领域的应用都有相当的潜力。以下是一些可能的应用领域:

+
    +
  1. 流体动力学模拟:DeepONet可以用于对流体动力学方程进行数值求解,例如Navier-Stokes方程。这使得DeepONet在诸如空气动力学、流体机械、气候模拟等领域具有直接应用。
  2. +
  3. 图像处理和计算机视觉:DeepONet可以学习图像中的特征,并用于分类、分割、检测等任务。例如,它可以用于医学图像分析,包括疾病检测和预后预测。
  4. +
  5. 信号处理:DeepONet可以用于各种信号处理任务,如降噪、压缩、恢复等。在通信、雷达、声纳等领域,DeepONet有潜在的应用。
  6. +
  7. 控制系统:DeepONet可以用于控制系统的设计和优化。例如,它可以学习系统的动态行为,并用于预测和控制系统的未来行为。
  8. +
  9. 金融:DeepONet可以用于金融预测和分析,如股票价格预测、风险评估、信贷风险分析等。
  10. +
  11. 人机交互:DeepONet可以用于语音识别、自然语言处理、手势识别等任务,使得人机交互更加智能化和自然。
  12. +
  13. 环境科学:DeepONet可以用于气候模型预测、生态系统的模拟、环境污染检测等任务。
  14. +
+

需要注意的是,虽然 DeepONet 在许多领域都有潜在的应用,但每个领域都有其独特的问题和挑战。在将 DeepONet 应用到特定领域时,需要对该领域的问题有深入的理解,并可能需要针对该领域进行模型的调整和优化。

+

2. 问题定义

+

假设存在如下 ODE 系统:

+
\[ +\begin{equation} +\left\{\begin{array}{l} +\frac{d}{d x} \mathbf{s}(x)=\mathbf{g}(\mathbf{s}(x), u(x), x) \\ +\mathbf{s}(a)=s_0 +\end{array}\right. +\end{equation} +\]
+

其中 \(u \in V\)(且 \(u\)\([a, b]\) 上连续)作为输入信号,\(\mathbf{s}: [a,b] \rightarrow \mathbb{R}^K\) 是该方程的解,作为输出信号。 +因此可以定义一种算子 \(G\),它满足:

+
\[ +\begin{equation} +(G u)(x)=s_0+\int_a^x \mathbf{g}((G u)(t), u(t), t) d t +\end{equation} +\]
+

因此可以利用神经网络模型,以 \(u\)\(x\) 为输入,\(G(u)(x)\) 为输出,进行监督训练来拟合 \(G\) 算子本身。

+

注:根据上述公式,可以发现算子 \(G\) 是一种积分算子 "\(\int\)",其作用在给定函数 \(u\) 上能求得其符合某种初值条件(本问题中初值条件为 \(G(u)(0)=0\))下的原函数 \(G(u)\)

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

本案例数据集使用 DeepXDE 官方文档提供的数据集,一个 npz 文件内已包含训练集和验证集,下载地址

+

数据文件说明如下:

+

antiderivative_unaligned_train.npz

+ + + + + + + + + + + + + + + + + + + + + +
字段名说明
X_train0\(u\) 对应的训练输入数据,形状为(10000, 100)
X_train1\(y\) 对应的训练输入数据数据,形状为(10000, 1)
y_train\(G(u)\) 对应的训练标签数据,形状为(10000,1)
+

antiderivative_unaligned_test.npz

+ + + + + + + + + + + + + + + + + + + + + +
字段名说明
X_test0\(u\) 对应的测试输入数据,形状为(100000, 100)
X_test1\(y\) 对应的测试输入数据数据,形状为(100000, 1)
y_test\(G(u)\) 对应的测试标签数据,形状为(100000,1)
+

3.2 模型构建

+

在上述问题中,我们确定了输入为 \(u\)\(y\),输出为 \(G(u)\),按照 DeepONet 论文所述,我们使用含有 branch 和 trunk 两个子分支网络的 DeepONet 来创建网络模型,用 PaddleScience 代码表示如下:

+
model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 uy,输出变量名是 G,接着通过指定 DeepONet 的 SENSORS 个数,特征通道数、隐藏层层数、神经元个数以及子网络的激活函数,我们就实例化出了 DeepONet 神经网络模型 model

+

3.3 约束构建

+

本文采用监督学习的方式,对模型输出 \(G(u)\) 进行约束。

+

在定义约束之前,需要给监督约束指定文件路径等数据读取配置,包括文件路径、输入数据字段名、标签数据字段名、数据转换前后的别名字典。

+
30
+31
+32
+33
+34
+35
+36
+37
+38
train_dataloader_cfg = {
+    "dataset": {
+        "name": "IterableNPZDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": ("u", "y"),
+        "label_keys": ("G",),
+        "alias_dict": {"u": "X_train0", "y": "X_train1", "G": "y_train"},
+    },
+}
+
+

3.3.1 监督约束

+

由于我们以监督学习方式进行训练,此处采用监督约束 SupervisedConstraint

+
40
+41
+42
+43
+44
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    {"G": lambda out: out["G"]},
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,此处填入在 3.4 约束构建 章节中实例化好的 train_dataloader_cfg

+

第二个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 为默认值 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第三个参数是方程表达式,用于描述如何计算约束目标,此处我们只需要从输出字典中,获取输出 G 这个字段对应的输出即可;

+

在监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.4 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用一万轮训练轮数,并每隔 500 个 epochs 评估一次模型精度。

+
49
+50
+51
+52
+53
+54
+55
TRAIN:
+  epochs: 10000
+  iters_per_epoch: 1
+  learning_rate: 1.0e-3
+  save_freq: 500
+  eval_freq: 500
+  eval_during_train: true
+
+

3.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,学习率设置为 0.001

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.6 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器。

+
51
+52
+53
+54
+55
+56
+57
+58
+59
+60
# set validator
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "IterableNPZDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": ("u", "y"),
+        "label_keys": ("G",),
+        "alias_dict": {"u": "X_test0", "y": "X_test1", "G": "y_test"},
+    },
+}
+
+

评价指标 metric 选择 ppsci.metric.L2Rel 即可。

+

其余配置与 约束构建 的设置类似。

+

3.7 模型训练、评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    eval_freq=cfg.TRAIN.eval_freq,
+    log_freq=cfg.log_freq,
+    seed=cfg.seed,
+    validator=validator,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.8 结果可视化

+

在模型训练完毕之后,我们可以手动构造 \(u\)\(y\) 并在适当范围内进行离散化,得到对应输入数据,继而预测出 \(G(u)(y)\),并和 \(G(u)\) 的标准解共同绘制图像,进行对比。(此处我们构造了 9 组 \(u-G(u)\) 函数对)进行测试

+
    def predict_func(input_dict):
+        return solver.predict(input_dict, return_numpy=True)[cfg.MODEL.G_key]
+
+    plot(cfg, predict_func)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableNPZDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": ("u", "y"),
+            "label_keys": ("G",),
+            "alias_dict": {"u": "X_test0", "y": "X_test1", "G": "y_test"},
+        },
+    }
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        {"G": lambda out: out["G"]},
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="G_eval",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        None,
+        cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    solver.eval()
+
+    def predict_func(input_dict):
+        return solver.predict(input_dict, return_numpy=True)[cfg.MODEL.G_key]
+
+    plot(cfg, predict_func)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+
+

4. 完整代码

+
deeponet.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
"""
+Reference: https://deepxde.readthedocs.io/en/latest/demos/operator/antiderivative_unaligned.html
+"""
+
+import os
+from os import path as osp
+from typing import Callable
+from typing import Tuple
+
+import hydra
+import numpy as np
+import paddle
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableNPZDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": ("u", "y"),
+            "label_keys": ("G",),
+            "alias_dict": {"u": "X_train0", "y": "X_train1", "G": "y_train"},
+        },
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        {"G": lambda out: out["G"]},
+    )
+    # wrap constraints together
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableNPZDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": ("u", "y"),
+            "label_keys": ("G",),
+            "alias_dict": {"u": "X_test0", "y": "X_test1", "G": "y_test"},
+        },
+    }
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        {"G": lambda out: out["G"]},
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="G_eval",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        eval_freq=cfg.TRAIN.eval_freq,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    def predict_func(input_dict):
+        return solver.predict(input_dict, return_numpy=True)[cfg.MODEL.G_key]
+
+    plot(cfg, predict_func)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableNPZDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": ("u", "y"),
+            "label_keys": ("G",),
+            "alias_dict": {"u": "X_test0", "y": "X_test1", "G": "y_test"},
+        },
+    }
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        {"G": lambda out: out["G"]},
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="G_eval",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        None,
+        cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    solver.eval()
+
+    def predict_func(input_dict):
+        return solver.predict(input_dict, return_numpy=True)[cfg.MODEL.G_key]
+
+    plot(cfg, predict_func)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.DeepONet(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            model.input_keys[0]: InputSpec(
+                [None, 1000], "float32", name=model.input_keys[0]
+            ),
+            model.input_keys[1]: InputSpec(
+                [None, 1], "float32", name=model.input_keys[1]
+            ),
+        }
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy import python_infer
+
+    predictor = python_infer.GeneralPredictor(cfg)
+
+    def predict_func(input_dict):
+        return next(iter(predictor.predict(input_dict).values()))
+
+    plot(cfg, predict_func)
+
+
+def plot(cfg: DictConfig, predict_func: Callable):
+    # visualize prediction for different functions u and corresponding G(u)
+    dtype = paddle.get_default_dtype()
+
+    def generate_y_u_G_ref(
+        u_func: Callable, G_u_func: Callable
+    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+        """Generate discretized data of given function u and corresponding G(u).
+
+        Args:
+            u_func (Callable): Function u.
+            G_u_func (Callable): Function G(u).
+
+        Returns:
+            Tuple[np.ndarray, np.ndarray, np.ndarray]: Discretized data of u, y and G(u).
+        """
+        x = np.linspace(0, 1, cfg.MODEL.num_loc, dtype=dtype).reshape(
+            [1, cfg.MODEL.num_loc]
+        )
+        u = u_func(x)
+        u = np.tile(u, [cfg.NUM_Y, 1])
+
+        y = np.linspace(0, 1, cfg.NUM_Y, dtype=dtype).reshape([cfg.NUM_Y, 1])
+        G_ref = G_u_func(y)
+        return u, y, G_ref
+
+    func_u_G_pair = [
+        # (title_string, func_u, func_G(u)), s.t. dG/dx == u and G(u)(0) = 0
+        (r"$u=\cos(x), G(u)=sin(x$)", lambda x: np.cos(x), lambda y: np.sin(y)),  # 1
+        (
+            r"$u=sec^2(x), G(u)=tan(x$)",
+            lambda x: (1 / np.cos(x)) ** 2,
+            lambda y: np.tan(y),
+        ),  # 2
+        (
+            r"$u=sec(x)tan(x), G(u)=sec(x) - 1$",
+            lambda x: (1 / np.cos(x) * np.tan(x)),
+            lambda y: 1 / np.cos(y) - 1,
+        ),  # 3
+        (
+            r"$u=1.5^x\ln{1.5}, G(u)=1.5^x-1$",
+            lambda x: 1.5**x * np.log(1.5),
+            lambda y: 1.5**y - 1,
+        ),  # 4
+        (r"$u=3x^2, G(u)=x^3$", lambda x: 3 * x**2, lambda y: y**3),  # 5
+        (r"$u=4x^3, G(u)=x^4$", lambda x: 4 * x**3, lambda y: y**4),  # 6
+        (r"$u=5x^4, G(u)=x^5$", lambda x: 5 * x**4, lambda y: y**5),  # 7
+        (r"$u=6x^5, G(u)=x^6$", lambda x: 5 * x**4, lambda y: y**5),  # 8
+        (r"$u=e^x, G(u)=e^x-1$", lambda x: np.exp(x), lambda y: np.exp(y) - 1),  # 9
+    ]
+
+    os.makedirs(os.path.join(cfg.output_dir, "visual"), exist_ok=True)
+    for i, (title, u_func, G_func) in enumerate(func_u_G_pair):
+        u, y, G_ref = generate_y_u_G_ref(u_func, G_func)
+        G_pred = predict_func({"u": u, "y": y})
+        plt.plot(y, G_pred, label=r"$G(u)(y)_{ref}$")
+        plt.plot(y, G_ref, label=r"$G(u)(y)_{pred}$")
+        plt.legend()
+        plt.title(title)
+        plt.savefig(os.path.join(cfg.output_dir, "visual", f"func_{i}_result.png"))
+        logger.message(
+            f"Saved result of function {i} to {cfg.output_dir}/visual/func_{i}_result.png"
+        )
+        plt.clf()
+    plt.close()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="deeponet.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+
+

result0.jpg + result1.jpg + result2.jpg + result3.jpg + result4.jpg + result5.jpg + result6.jpg + result7.jpg + result8.jpg

+
+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/dgmr/index.html b/zh/examples/dgmr/index.html new file mode 100644 index 0000000000..81623abbab --- /dev/null +++ b/zh/examples/dgmr/index.html @@ -0,0 +1,4406 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DGMR - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

DGMR(deep generative models of radar)

+
+
+
+

暂无

+
+
+
# Download data from Huggingface
+mkdir openclimatefix/nimrod-uk-1km/20200718/valid/subsampled_tiles_256_20min_stride
+cd openclimatefix/nimrod-uk-1km/20200718/valid/subsampled_tiles_256_20min_stride
+git lfs install
+git lfs pull --include="seq-24-*-of-00033.tfrecord.gz"
+
+python dgmr.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/dgmr/dgmr_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
dgmr_pretrained.pdparamsd_loss: 127.041
g_loss: 59.2409
grid_loss: 1.7699
+

1. 背景简介

+

短临降水预报是对未来两小时内的降水进行高分辨率预测,支持了许多依赖于天气决策的实际社会经济需求。最先进的运行即时预报方法通常利用基于雷达的风估计对降水场进行平流,但往往难以捕捉重要的非线性事件,如对流的发生。最近引入的深度学习方法利用雷达直接预测未来的降雨率,摆脱了物理约束。虽然它们能够准确预测低强度降雨,但由于缺乏约束,在更长的前瞻时间内产生模糊的即时预报,导致对中到大雨事件的性能较差,其运行实用性受到限制。为了解决这些挑战,我们提出了一种用于基于雷达的降水概率即时预报的深度生成模型。我们的模型在范围为1536 km × 1280 km的区域内,能够在5-90分钟的前瞻时间内生成逼真且时空一致的预测。通过英国气象局的五十多位专家预报员进行的系统评估显示,我们的生成模型在88%的情况下在准确性和实用性方面排名第一,超过了两种竞争方法,表明了其在决策价值和向现实世界专家提供物理洞察力方面的能力。在定量验证方面,这些即时预报具有良好的技能而无需进行模糊处理。我们展示了生成式即时预报可以提供改进预报价值并支持运行实用性的概率性预测,在分辨率和前瞻时间方面,替代方法存在困难的情况下尤其如此。

+

短临降水预报在工程和科学领域具有多方面的重要性,主要体现在以下几个方面:

+
    +
  • 社会影响: 短临降水预报对各行各业的实际决策都有着直接的社会影响。例如,农业、水资源管理、城市防汛和交通运输等领域都需要准确的降水预报来做出相应的应对措施,以减少可能的损失和风险。
  • +
  • 安全保障: 短临降水预报对于保障公众安全至关重要。例如,预警系统可以根据短临降水预报提前通知人们可能发生的暴雨、洪涝、泥石流等灾害,从而及时采取避险措施,减少人员伤亡和财产损失。
  • +
  • 生态环境保护: 对降水的准确预报有助于生态环境的保护和管理。例如,预测降水量可以帮助决策者及时调整水利工程的运行,保障生态系统的健康运行,并为植被生长提供必要的水资源。
  • +
  • 科学研究: 短临降水预报也是气象科学研究的重要组成部分。通过对降水过程的研究和预测,可以更好地理解大气环境中的水循环过程,为气象学、气候学等相关领域的研究提供重要数据和支撑。
  • +
  • 工程规划和设计: 在城市规划、土木工程、农业灌溉等领域,准确的短临降水预报对工程的规划和设计至关重要。例如,在城市排水系统设计中,需要考虑未来短时间内可能发生的降水情况,以保证排水系统的正常运行和城市的防洪能力。
  • +
+

总的来说,短临降水预报在工程和科学领域的重要性体现在保障社会安全、促进科学研究、支持生态环境保护和推动工程发展等多个方面,对于各行业的发展和社会的可持续发展都具有重要意义。

+

2. 模型原理

+

2.1 模型结构

+

DGMR是在条件生成对抗网络的算法框架中构建的。以过去的雷达数据为基础,对未来的雷达做出详细和可信的预测。也就是说在给定的时间点 \(T\),使用基于雷达的地表降水估计值 \(X_T\),基于过去 \(M\) 个雷达场预测未来 \(N\) 个雷达场。即:

+
\[ +P\left(X_{M+1: M+N} \mid X_{1: M}\right)=\int P\left(X_{M+1: M+N} \mid \mathrm{Z}, X_{1: M}, \boldsymbol{\theta}\right) P\left(\mathrm{Z} \mid X_{1: M}\right) d \mathrm{Z}. +\]
+

其中 \(Z\) 为随机向量,\(\theta\) 为生成模型的参数。

+
    +
  • 等式左边是条件概率,给定过去\(M\)个时刻的雷达降水,预报之后\(N\)个时刻的雷达降水。
  • +
  • 右边则将概率写为集合预报的积分形式:
  • +
  • 给定随机抽样 \(Z\) 和生成网络参数 \(\theta\),在过去 \(M\) 个时刻的雷达降水约束下预报之后 \(N\) 个时刻的雷达降水;
  • +
  • 计算随机抽样 \(Z\) 在过去 \(M\) 个时刻的雷达降水约束下的条件概率;
  • +
  • 两者相乘,为该结果的出现概率,积分后得到多次抽样下的集合预报。
  • +
+

对随机向量 \(Z\) 的积分确保了模型产生的预测具有空间相关性。DGMR 专门用于降水预测问题。四个连续的雷达观测数据(前20分钟)被用作生成器的输入,该生成器允许对未来降水的多个实现进行抽样,每个实现包含18帧(90分钟)。模型架构示意图如图所示。

+
+

domain_chip.pdf +

+
模型架构示意图。
+
+

DGMR 是一个使用两个判别器和一个附加正则化项进行训练的生成器。下图显示了生成模型和判别器的详细示意图:

+
+

domain_chip.pdf +

+
a、生成器架构。b,生成器的时间鉴别器架构(左上)、空间鉴别器(左中)和潜在条件堆栈(左下)。右侧是 G 块(上)、D 和 3D 块(中)以及 L 块(右)的架构。
+
+

2.2 目标函数

+

生成器通过两个鉴别器的损失和一个网格单元正则化项(记为 \(\mathcal{L}_R(\theta)\) )进行训练。空间鉴别器 \(D\phi\) 具有参数 \(\phi\),时间鉴别器 \(T_\psi\) 具有参数 \(\psi\),生成器 \(G_\theta\) 具有参数 \(\theta\)。我们使用符号 \(\{X ; G\}\) 表示两个字段的串联。最大化的生成器损失如下:

+
\[ +\begin{gathered} +\mathcal{L}_G(\theta)=\mathbb{E}_{X_{1: M+N}}\left[\mathbb{E}_Z\left[D\left(G_\theta\left(Z ; X_{1: M}\right)\right)+T\left(\left\{X_{1: M} ; G_\theta\left(Z ; X_{1: M}\right)\right\}\right)\right]-\lambda \mathcal{L}_R(\theta)\right] ; \\ +\mathcal{L}_R(\theta)=\frac{1}{H W N}\left\|\left(\mathbb{E}_Z\left[G_\theta\left(Z ; X_{1: M}\right)\right]-X_{M+1: M+N}\right) \odot w\left(X_{M+1: M+N}\right)\right\|_1 . +\end{gathered} +\]
+

我们在上面公式中对潜变量 \(\mathrm{Z}\) 的期望使用 Carlo 估计。这些估计是使用每个输入 \(X_{1: M}\) 的六个样本计算的,其中包括 \(M=4\) 个雷达观测数据。网格单元正则化项确保平均预测保持接近真实值,并在高度 \(H\)、宽度 \(W\) 和提前时间 \(N\) 轴上对所有网格单元进行平均。它通过函数 \(w(y)=\max (y+1,24)\) 加权至更高的降雨目标,该函数对输入向量进行逐元素操作,并在 24 处截断以提高对雷达中异常大值的鲁棒性。GAN 空间鉴别器损失 \(\mathcal{L}_D(\phi)\) 和时间鉴别器损失 \(\mathcal{L}_T(\psi)\) 分别相对于参数 \(\phi\)\(\psi\) 最小化。鉴别器损失采用铰链损失公式:

+
\[ +\begin{aligned} +& \mathcal{L}_D(\phi)=\mathbb{E}_{X_{1: M+N}, Z}\left[\operatorname{ReLU}\left(1-D_\phi\left(X_{M+1: M+N}\right)\right)+\operatorname{ReLU}\left(1+D_\phi\left(G\left(Z ; X_{1: M}\right)\right)\right)\right], \\ +& \mathcal{L}_T(\psi)=\mathbb{E}_{X_{1: M+N}, Z}\left[\operatorname{ReLU}\left(1-T_\psi\left(X_{1: M+N}\right)\right)+\operatorname{ReLU}\left(1+T_\psi\left(\left\{X_{1: M} ; G\left(Z ; X_{1: M}\right)\right\}\right)\right)\right], +\end{aligned} +\]
+

其中 \(\operatorname{ReLU} = \max(0,x)\). 更多详细的理论推导请参考 Skillful Precipitation Nowcasting using Deep Generative Models of Radar

+

3. 问题求解

+

接下来开始讲解如何将该问题一步一步地转化为 PaddleScience 代码,用 DGMR 来预测短临降水。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建等关键步骤进行阐述,而其余细节请参考API文档

+

3.1 数据集介绍

+

为了训练和评估英国的临近预报模型,DGMR 使用了英国气象局 RadarNet4 网络中的雷达复合数据。使用 2016 年 1 月 1 日至 2019 年 12 月 31 日期间每五分钟收集一次的雷达数据。我们使用以下数据分割进行模型开发。将 2016 年至 2018 年每月第一天的字段分配给验证集。2016 年至 2018 年的所有其他日期都分配给训练集。最后,使用2019年的数据作为测试集,防止数据泄露和分布泛化测试。开源的英国训练数据集已镜像到 HuggingFace 数据集,用户可以自行下载使用。

+

在该模型的PaddleScience 代码中,我们可以调用ppsci.data.dataset.DGMRDataset 来加载数据集,代码如下:

+
# load evaluate data
+dataset = ppsci.data.dataset.DGMRDataset(**cfg.DATASET)
+val_loader = paddle.io.DataLoader(dataset, batch_size=4)
+
+

3.2 模型构建

+

在 DGMR 模型中,输入过去四个雷达场数据,对 18 个未来雷达场(接下来的 90 分钟)进行预测。DGMR 网络可以表示为 \(X_{1:4}\) 到输出 \(X_{5:22}\) 的映射函数 \(f\),即:

+
\[ +X_{5:22} = f(X_{1:4}),\\ +\]
+

上式中 \(f\) 代表 DGMR 模型。我们定义 PaddleScience 内置的 DGMR 模型类,并调用,PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.DGMR(**cfg.MODEL)
+
+

这样我们就实例化出了一个 DGMR 模型,模型参数设置相关内容请参考文献 Skillful Precipitation Nowcasting using Deep Generative Models of Radar

+

3.3 模型评估、可视化

+

模型构建和加载数据后,将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动评估和可视化。首先我们初始化模型:

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    pretrained_model_path=cfg.EVAL.pretrained_model_path,
+)
+solver.model.eval()
+
+

然后自定义评估方式和损失函数,代码如下:

+
def validation(
+    cfg: DictConfig,
+    solver: ppsci.solver.Solver,
+    batch: Tuple[Dict[str, paddle.Tensor], ...],
+):
+    """
+    validation step.
+
+    Args:
+        cfg (DictConfig): Configuration object.
+        solver (ppsci.solver.Solver): Solver object containing the model and related components.
+        batch (Tuple[Dict[str, paddle.Tensor], ...]): Input batch consisting of images and corresponding future images.
+
+    Returns:
+        discriminator_loss: Loss incurred by the discriminator.
+        generator_loss: Loss incurred by the generator.
+        grid_cell_reg: Regularization term to encourage smooth transitions.
+    """
+    images, future_images = batch
+    images_value = images[cfg.DATASET.input_keys[0]]
+    future_images_value = future_images[cfg.DATASET.label_keys[0]]
+    # Two discriminator steps per generator step
+    for _ in range(2):
+        predictions = solver.predict(images)
+        predictions_value = predictions[cfg.MODEL.output_keys[0]]
+        generated_sequence = paddle.concat(x=[images_value, predictions_value], axis=1)
+        real_sequence = paddle.concat(x=[images_value, future_images_value], axis=1)
+        concatenated_inputs = paddle.concat(
+            x=[real_sequence, generated_sequence], axis=0
+        )
+        concatenated_outputs = solver.model.discriminator(concatenated_inputs)
+        score_real, score_generated = paddle.split(
+            x=concatenated_outputs,
+            num_or_sections=[real_sequence.shape[0], generated_sequence.shape[0]],
+            axis=0,
+        )
+        score_real_spatial, score_real_temporal = paddle.split(
+            x=score_real, num_or_sections=score_real.shape[1], axis=1
+        )
+        score_generated_spatial, score_generated_temporal = paddle.split(
+            x=score_generated, num_or_sections=score_generated.shape[1], axis=1
+        )
+        discriminator_loss = _loss_hinge_disc(
+            score_generated_spatial, score_real_spatial
+        ) + _loss_hinge_disc(score_generated_temporal, score_real_temporal)
+
+    predictions_value = [
+        solver.predict(images)[cfg.MODEL.output_keys[0]] for _ in range(6)
+    ]
+    grid_cell_reg = _grid_cell_regularizer(
+        paddle.stack(x=predictions_value, axis=0), future_images_value
+    )
+    generated_sequence = [
+        paddle.concat(x=[images_value, x], axis=1) for x in predictions_value
+    ]
+    real_sequence = paddle.concat(x=[images_value, future_images_value], axis=1)
+    generated_scores = []
+    for g_seq in generated_sequence:
+        concatenated_inputs = paddle.concat(x=[real_sequence, g_seq], axis=0)
+        concatenated_outputs = solver.model.discriminator(concatenated_inputs)
+        score_real, score_generated = paddle.split(
+            x=concatenated_outputs,
+            num_or_sections=[real_sequence.shape[0], g_seq.shape[0]],
+            axis=0,
+        )
+        generated_scores.append(score_generated)
+    generator_disc_loss = _loss_hinge_gen(paddle.concat(x=generated_scores, axis=0))
+    generator_loss = generator_disc_loss + 20 * grid_cell_reg
+
+    return discriminator_loss, generator_loss, grid_cell_reg
+
+
+def _loss_hinge_disc(score_generated, score_real):
+    """Discriminator hinge loss."""
+    l1 = nn.functional.relu(x=1.0 - score_real)
+    loss = paddle.mean(x=l1)
+    l2 = nn.functional.relu(x=1.0 + score_generated)
+    loss += paddle.mean(x=l2)
+    return loss
+
+
+def _loss_hinge_gen(score_generated):
+    """Generator hinge loss."""
+    loss = -paddle.mean(x=score_generated)
+    return loss
+
+
+def _grid_cell_regularizer(generated_samples, batch_targets):
+    """Grid cell regularizer.
+
+    Args:
+      generated_samples: Tensor of size [n_samples, batch_size, 18, 256, 256, 1].
+      batch_targets: Tensor of size [batch_size, 18, 256, 256, 1].
+
+    Returns:
+      loss: A tensor of shape [batch_size].
+    """
+    gen_mean = paddle.mean(x=generated_samples, axis=0)
+    weights = paddle.clip(x=batch_targets, min=0.0, max=24.0)
+    loss = paddle.mean(x=paddle.abs(x=gen_mean - batch_targets) * weights)
+    return loss
+
+

最后对数据中的每个 batch 进行遍历评估,同时对预测结果进行可视化。

+
# evaluate pretrained model
+d_loss = []
+g_loss = []
+grid_loss = []
+for batch_idx, batch in enumerate(val_loader):
+    with paddle.no_grad():
+        out_dict = validation(cfg, solver, batch)
+
+        # visualize
+        images = batch[0][cfg.DATASET.input_keys[0]]
+        future_images = batch[1][cfg.DATASET.label_keys[0]]
+        generated_images = solver.predict(batch[0])[cfg.MODEL.output_keys[0]]
+        if batch_idx % 50 == 0:
+            logger.message(f"Saving plot of image frame to {cfg.output_dir}")
+            visualize(
+                cfg.output_dir, images, future_images, generated_images, batch_idx
+            )
+
+    d_loss.append(out_dict[0])
+    g_loss.append(out_dict[1])
+    grid_loss.append(out_dict[2])
+logger.message(f"d_loss: {np.array(d_loss).mean()}")
+logger.message(f"g_loss: {np.array(g_loss).mean()}")
+logger.message(f"grid_loss: {np.array(grid_loss).mean()}")
+
+

4. 完整代码

+
dgmr.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/openclimatefix/skillful_nowcasting
+"""
+from os import path as osp
+from typing import Dict
+from typing import Tuple
+
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+import paddle.nn as nn
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def visualize(
+    output_dir: str,
+    x: paddle.Tensor,
+    y: paddle.Tensor,
+    y_hat: paddle.Tensor,
+    batch_idx: int,
+) -> None:
+    """
+    Visualizes input, target, and generated images and saves them to the output directory.
+
+    Args:
+        output_dir (str): Directory to save the visualization images.
+        x (paddle.Tensor): Input images tensor.
+        y (paddle.Tensor): Target images tensor.
+        y_hat (paddle.Tensor): Generated images tensor.
+        batch_idx (int): Batch index.
+
+    Returns:
+        None
+    """
+    images = x[0]
+    future_images = y[0]
+    generated_images = y_hat[0]
+    fig, axes = plt.subplots(2, 2)
+    for i, ax in enumerate(axes.flat):
+        alpha = images[i][0].numpy()
+        alpha[alpha < 1] = 0
+        alpha[alpha > 1] = 1
+        ax.imshow(images[i].transpose([1, 2, 0]).numpy(), alpha=alpha, cmap="viridis")
+    plt.subplots_adjust(hspace=0.1, wspace=0.1)
+    plt.savefig(osp.join(output_dir, f"Input_Image_Stack_Frame_{batch_idx}.png"))
+    fig, axes = plt.subplots(3, 3)
+    for i, ax in enumerate(axes.flat):
+        alpha = future_images[i][0].numpy()
+        alpha[alpha < 1] = 0
+        alpha[alpha > 1] = 1
+        ax.imshow(
+            future_images[i].transpose([1, 2, 0]).numpy(), alpha=alpha, cmap="viridis"
+        )
+    plt.subplots_adjust(hspace=0.1, wspace=0.1)
+    plt.savefig(osp.join(output_dir, f"Target_Image_Frame_{batch_idx}.png"))
+    fig, axes = plt.subplots(3, 3)
+    for i, ax in enumerate(axes.flat):
+        alpha = generated_images[i][0].numpy()
+        alpha[alpha < 1] = 0
+        alpha[alpha > 1] = 1
+        ax.imshow(
+            generated_images[i].transpose([1, 2, 0]).numpy(),
+            alpha=alpha,
+            cmap="viridis",
+        )
+    plt.subplots_adjust(hspace=0.1, wspace=0.1)
+    plt.savefig(osp.join(output_dir, f"Generated_Image_Frame_{batch_idx}.png"))
+    plt.close()
+
+
+def validation(
+    cfg: DictConfig,
+    solver: ppsci.solver.Solver,
+    batch: Tuple[Dict[str, paddle.Tensor], ...],
+):
+    """
+    validation step.
+
+    Args:
+        cfg (DictConfig): Configuration object.
+        solver (ppsci.solver.Solver): Solver object containing the model and related components.
+        batch (Tuple[Dict[str, paddle.Tensor], ...]): Input batch consisting of images and corresponding future images.
+
+    Returns:
+        discriminator_loss: Loss incurred by the discriminator.
+        generator_loss: Loss incurred by the generator.
+        grid_cell_reg: Regularization term to encourage smooth transitions.
+    """
+    images, future_images = batch
+    images_value = images[cfg.DATASET.input_keys[0]]
+    future_images_value = future_images[cfg.DATASET.label_keys[0]]
+    # Two discriminator steps per generator step
+    for _ in range(2):
+        predictions = solver.predict(images)
+        predictions_value = predictions[cfg.MODEL.output_keys[0]]
+        generated_sequence = paddle.concat(x=[images_value, predictions_value], axis=1)
+        real_sequence = paddle.concat(x=[images_value, future_images_value], axis=1)
+        concatenated_inputs = paddle.concat(
+            x=[real_sequence, generated_sequence], axis=0
+        )
+        concatenated_outputs = solver.model.discriminator(concatenated_inputs)
+        score_real, score_generated = paddle.split(
+            x=concatenated_outputs,
+            num_or_sections=[real_sequence.shape[0], generated_sequence.shape[0]],
+            axis=0,
+        )
+        score_real_spatial, score_real_temporal = paddle.split(
+            x=score_real, num_or_sections=score_real.shape[1], axis=1
+        )
+        score_generated_spatial, score_generated_temporal = paddle.split(
+            x=score_generated, num_or_sections=score_generated.shape[1], axis=1
+        )
+        discriminator_loss = _loss_hinge_disc(
+            score_generated_spatial, score_real_spatial
+        ) + _loss_hinge_disc(score_generated_temporal, score_real_temporal)
+
+    predictions_value = [
+        solver.predict(images)[cfg.MODEL.output_keys[0]] for _ in range(6)
+    ]
+    grid_cell_reg = _grid_cell_regularizer(
+        paddle.stack(x=predictions_value, axis=0), future_images_value
+    )
+    generated_sequence = [
+        paddle.concat(x=[images_value, x], axis=1) for x in predictions_value
+    ]
+    real_sequence = paddle.concat(x=[images_value, future_images_value], axis=1)
+    generated_scores = []
+    for g_seq in generated_sequence:
+        concatenated_inputs = paddle.concat(x=[real_sequence, g_seq], axis=0)
+        concatenated_outputs = solver.model.discriminator(concatenated_inputs)
+        score_real, score_generated = paddle.split(
+            x=concatenated_outputs,
+            num_or_sections=[real_sequence.shape[0], g_seq.shape[0]],
+            axis=0,
+        )
+        generated_scores.append(score_generated)
+    generator_disc_loss = _loss_hinge_gen(paddle.concat(x=generated_scores, axis=0))
+    generator_loss = generator_disc_loss + 20 * grid_cell_reg
+
+    return discriminator_loss, generator_loss, grid_cell_reg
+
+
+def _loss_hinge_disc(score_generated, score_real):
+    """Discriminator hinge loss."""
+    l1 = nn.functional.relu(x=1.0 - score_real)
+    loss = paddle.mean(x=l1)
+    l2 = nn.functional.relu(x=1.0 + score_generated)
+    loss += paddle.mean(x=l2)
+    return loss
+
+
+def _loss_hinge_gen(score_generated):
+    """Generator hinge loss."""
+    loss = -paddle.mean(x=score_generated)
+    return loss
+
+
+def _grid_cell_regularizer(generated_samples, batch_targets):
+    """Grid cell regularizer.
+
+    Args:
+      generated_samples: Tensor of size [n_samples, batch_size, 18, 256, 256, 1].
+      batch_targets: Tensor of size [batch_size, 18, 256, 256, 1].
+
+    Returns:
+      loss: A tensor of shape [batch_size].
+    """
+    gen_mean = paddle.mean(x=generated_samples, axis=0)
+    weights = paddle.clip(x=batch_targets, min=0.0, max=24.0)
+    loss = paddle.mean(x=paddle.abs(x=gen_mean - batch_targets) * weights)
+    return loss
+
+
+def train(cfg: DictConfig):
+    raise NotImplementedError("Training of DGMR is not supported now.")
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.DGMR(**cfg.MODEL)
+    # load evaluate data
+    dataset = ppsci.data.dataset.DGMRDataset(**cfg.DATASET)
+    val_loader = paddle.io.DataLoader(dataset, batch_size=4)
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.model.eval()
+
+    # evaluate pretrained model
+    d_loss = []
+    g_loss = []
+    grid_loss = []
+    for batch_idx, batch in enumerate(val_loader):
+        with paddle.no_grad():
+            out_dict = validation(cfg, solver, batch)
+
+            # visualize
+            images = batch[0][cfg.DATASET.input_keys[0]]
+            future_images = batch[1][cfg.DATASET.label_keys[0]]
+            generated_images = solver.predict(batch[0])[cfg.MODEL.output_keys[0]]
+            if batch_idx % 50 == 0:
+                logger.message(f"Saving plot of image frame to {cfg.output_dir}")
+                visualize(
+                    cfg.output_dir, images, future_images, generated_images, batch_idx
+                )
+
+        d_loss.append(out_dict[0])
+        g_loss.append(out_dict[1])
+        grid_loss.append(out_dict[2])
+    logger.message(f"d_loss: {np.array(d_loss).mean()}")
+    logger.message(f"g_loss: {np.array(g_loss).mean()}")
+    logger.message(f"grid_loss: {np.array(grid_loss).mean()}")
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="dgmr.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

如图所示为分别在 \(T+5, T+10, \cdots, T+45\) 分钟的气象降水预测,与真实的降水情况相比可以看出,该模型可以给出比较好的短临降水预测。

+
+

chip.png +

+
模型预测的降水情况
+
+
+

chip.png +

+
真实降水情况
+
+

6. 参考资料

+

参考文献: Skillful Precipitation Nowcasting using Deep Generative Models of Radar

+

参考代码: DGMR

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/earthformer/index.html b/zh/examples/earthformer/index.html new file mode 100644 index 0000000000..dba7eeda9a --- /dev/null +++ b/zh/examples/earthformer/index.html @@ -0,0 +1,6574 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EarthFormer - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

EarthFormer

+

开始训练、评估前,请先下载

+

ICAR-ENSO数据集

+

SEVIR数据集

+
+
+
+
# ICAR-ENSO 数据预训练模型
+python examples/earthformer/earthformer_enso_train.py
+# SEVIR 数据集预训练模型
+python examples/earthformer/earthformer_sevir_train.py
+
+
+
+
# ICAR-ENSO 模型评估
+python examples/earthformer/earthformer_enso_train.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/earthformer/earthformer_enso.pdparams
+# SEVIR 模型评估
+python examples/earthformer/earthformer_sevir_train.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/earthformer/earthformer_sevir.pdparams
+
+
+
+
# ICAR-ENSO 模型推理
+python examples/earthformer/earthformer_enso_train.py mode=export
+# SEVIR 模型推理
+python examples/earthformer/earthformer_sevir_train.py mode=export
+
+
+
+
# ICAR-ENSO 模型推理
+python examples/earthformer/earthformer_enso_train.py mode=infer
+# SEVIR 模型推理
+python examples/earthformer/earthformer_sevir_train.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
模型变量名称C-Nino3.4-MC-Nino3.4-WMMSE(1E-4)
ENSO 模型sst0.741302.289902.5000
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模型变量名称CSI-MCSI-219CSI-181CSI-160CSI-133CSI-74CSI-16MSE(1E-4)
SEVIR 模型vil0.44190.17910.28480.32320.42710.68600.75133.6957
+

1. 背景简介

+

地球是一个复杂的系统。地球系统的变化,从温度波动等常规事件到干旱、冰雹和厄尔尼诺/南方涛动 (ENSO) 等极端事件,影响着我们的日常生活。在所有后果中,地球系统的变化会影响农作物产量、航班延误、引发洪水和森林火灾。对这些变化进行准确及时的预测可以帮助人们采取必要的预防措施以避免危机,或者更好地利用风能和太阳能等自然资源。因此,改进地球变化(例如天气和气候)的预测模型具有巨大的社会经济影响。

+

Earthformer,一种用于地球系统预测的时空转换器。为了更好地探索时空注意力的设计,论文提出了 Cuboid Attention ,它是高效时空注意力的通用构建块。这个想法是将输入张量分解为不重叠的长方体,并行应用长方体级自注意力。由于我们将 O(N2) 自注意力限制在局部长方体内,因此整体复杂度大大降低。不同类型的相关性可以通过不同的长方体分解来捕获。同时论文引入了一组关注所有局部长方体的全局向量,从而收集系统的整体状态。通过关注全局向量,局部长方体可以掌握系统的总体动态并相互共享信息。

+

2. 模型原理

+

本章节仅对 EarthFormer 的模型原理进行简单地介绍,详细的理论推导请阅读 Earthformer: Exploring Space-Time Transformers for Earth System Forecasting

+

Earthformer 的网络模型使用了基于 Cuboid Attention 的分层 Transformer incoder-decoder 。这个想法是将数据分解为长方体并并行应用长方体级自注意力。这些长方体进一步与全局向量的集合连接。

+

模型的总体结构如图所示:

+
+

Earthformer-arch +

+
EarthFormer 网络模型
+
+

EarthFormer 原代码中训练了 ICAR-ENSO 数据集中海面温度 (sst) 和 SEVIR 数据集中对云总降水量 (vil) 的估计模型,接下来将介绍这两个模型的训练、推理过程。

+

2.1 ICAR-ENSO 和 SEVIR 模型的训练、推理过程

+

模型预训练阶段是基于随机初始化的网络权重对模型进行训练,如下图所示,其中 \([x_{i}]_{i=1}^{T}\) 表示长度为 \(T\) 时空序列的输入气象数据,\([y_{i}]_{i=1}^{K}\) 表示预测未来 \(K\) 步的气象数据,\([y_{i_True}]_{i=1}^{K}\) 表示未来 \(K\) 步的真实数据,如海面温度数据和云总降水量数据。最后网络模型预测的输出和真值计算 mse 损失函数。

+
+

earthformer-pretraining +

+
earthformer 模型预训练
+
+

在推理阶段,给定长度序列为 \(T\) 的数据,得到长度序列为 \(K\) 的预测结果。

+
+

earthformer-pretraining +

+
earthformer 模型推理
+
+

3. 海面温度模型实现

+

接下来开始讲解如何基于 PaddleScience 代码,实现 EarthFormer 模型的训练与推理。关于该案例中的其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集采用了 EarthFormer 处理好的 ICAR-ENSO 数据集。

+

本数据集由气候与应用前沿研究院 ICAR 提供。数据包括 CMIP5/6 模式的历史模拟数据和美国 SODA 模式重建的近100多年历史观测同化数据。每个样本包含以下气象及时空变量:海表温度异常 (SST) ,热含量异常 (T300),纬向风异常 (Ua),经向风异常 (Va),数据维度为 (year,month,lat,lon)。训练数据提供对应月份的 Nino3.4 index 标签数据。测试用的初始场数据为国际多个海洋资料同化结果提供的随机抽取的 n 段 12 个时间序列,数据格式采用 NPY 格式保存。

+

训练数据:

+

每个数据样本第一维度 (year) 表征数据所对应起始年份,对于 CMIP 数据共 291 年,其中 1-2265 为 CMIP6 中 15 个模式提供的 151 年的历史模拟数据 (总共:151年 15 个模式=2265) ;2266-4645 为 CMIP5 中 17 个模式提供的 140 年的历史模拟数据 (总共:140 年17 个模式=2380)。对于历史观测同化数据为美国提供的 SODA 数据。

+

训练数据标签

+

标签数据为 Nino3.4 SST 异常指数,数据维度为 (year,month)。

+

CMIP(SODA)_train.nc 对应的标签数据当前时刻 Nino3.4 SST 异常指数的三个月滑动平均值,因此数据维度与维度介绍同训练数据一致。

+

注:三个月滑动平均值为当前月与未来两个月的平均值。

+

测试数据

+

测试用的初始场 (输入) 数据为国际多个海洋资料同化结果提供的随机抽取的 n 段 12 个时间序列,数据格式采用NPY格式保存,维度为 (12,lat,lon, 4), 12 为 t 时刻及过去 11 个时刻,4 为预测因子,并按照 SST,T300,Ua,Va 的顺序存放。

+

EarthFFormer 模型对于 ICAR-ENSO 数据集的训练中,只对其中海面温度 (SST) 进行训练和预测。训练海温异常观测的 12 步 (一年) ,预测海温异常最多 14 步。

+

3.2 模型预训练

+

3.2.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数。

+

数据加载的代码如下:

+
examples/earthformer/earthformer_enso_train.py
35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
train_dataloader_cfg = {
+    "dataset": {
+        "name": "ENSODataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "in_stride": cfg.DATASET.in_stride,
+        "out_stride": cfg.DATASET.out_stride,
+        "train_samples_gap": cfg.DATASET.train_samples_gap,
+        "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+        "normalize_sst": cfg.DATASET.normalize_sst,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 ENSODataset,"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,设置的 batch_size 为 16,num_works 为 8。

+

定义监督约束的代码如下:

+
examples/earthformer/earthformer_enso_train.py
58
+59
+60
+61
+62
+63
+64
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用自定义的损失函数 mse_loss

+

第三个参数是约束条件的名字,方便后续对其索引。此处命名为 Sup

+

3.2.2 模型构建

+

在该案例中,海面温度模型基于 CuboidTransformer 网络模型实现,用 PaddleScience 代码表示如下:

+
examples/earthformer/earthformer_enso_train.py
97
+98
+99
model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

网络模型的参数通过配置文件进行设置如下:

+
examples/earthformer/conf/earthformer_enso_pretrain.yaml
# model settings
+MODEL:
+  input_keys: ["sst_data"]
+  output_keys: ["sst_target","nino_target"]
+  input_shape: [12, 24, 48, 1]
+  target_shape: [14, 24, 48, 1]
+  base_units: 64
+  scale_alpha: 1.0
+
+  enc_depth: [1, 1]
+  dec_depth: [1, 1]
+  enc_use_inter_ffn: true
+  dec_use_inter_ffn: true
+  dec_hierarchical_pos_embed: false
+
+  downsample: 2
+  downsample_type: "patch_merge"
+  upsample_type: "upsample"
+
+  num_global_vectors: 0
+  use_dec_self_global: false
+  dec_self_update_global: true
+  use_dec_cross_global: false
+  use_global_vector_ffn: false
+  use_global_self_attn: false
+  separate_global_qkv: false
+  global_dim_ratio: 1
+
+  self_pattern: "axial"
+  cross_self_pattern: "axial"
+  cross_pattern: "cross_1x1"
+  dec_cross_last_n_frames: null
+
+  attn_drop: 0.1
+  proj_drop: 0.1
+  ffn_drop: 0.1
+  num_heads: 4
+
+  ffn_activation: "gelu"
+  gated_ffn: false
+  norm_layer: "layer_norm"
+  padding_type: "zeros"
+  pos_embed_type: "t+h+w"
+  use_relative_pos: true
+  self_attn_use_final_proj: true
+  dec_use_first_self_attn: false
+
+  z_init_method: "zeros"
+  initial_downsample_type: "conv"
+  initial_downsample_activation: "leaky_relu"
+  initial_downsample_scale: [1, 1, 2]
+  initial_downsample_conv_layers: 2
+  final_upsample_conv_layers: 1
+  checkpoint_level: 2
+
+  attn_linear_init_mode: "0"
+  ffn_linear_init_mode: "0"
+  conv_init_mode: "0"
+  down_up_linear_init_mode: "0"
+  norm_init_mode: "0"
+
+

其中,input_keysoutput_keys 分别代表网络模型输入、输出变量的名称。

+

3.2.3 学习率与优化器构建

+

本案例中使用的学习率方法为 Cosine,学习率大小设置为 2e-4。优化器使用 AdamW,并将参数进行分组,使用不同的 +weight_decay,用 PaddleScience 代码表示如下:

+
examples/earthformer/earthformer_enso_train.py
decay_parameters = get_parameter_names(model, [nn.LayerNorm])
+decay_parameters = [name for name in decay_parameters if "bias" not in name]
+optimizer_grouped_parameters = [
+    {
+        "params": [p for n, p in model.named_parameters() if n in decay_parameters],
+        "weight_decay": cfg.TRAIN.wd,
+    },
+    {
+        "params": [
+            p for n, p in model.named_parameters() if n not in decay_parameters
+        ],
+        "weight_decay": 0.0,
+    },
+]
+
+# # init optimizer and lr scheduler
+lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(
+    **lr_scheduler_cfg,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    eta_min=cfg.TRAIN.min_lr_ratio * cfg.TRAIN.lr_scheduler.learning_rate,
+    warmup_epoch=int(0.2 * cfg.TRAIN.epochs),
+)()
+optimizer = paddle.optimizer.AdamW(
+    lr_scheduler, parameters=optimizer_grouped_parameters
+)
+
+

3.2.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/earthformer/earthformer_enso_train.py
68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ENSODataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "in_stride": cfg.DATASET.in_stride,
+        "out_stride": cfg.DATASET.out_stride,
+        "train_samples_gap": cfg.DATASET.train_samples_gap,
+        "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+        "normalize_sst": cfg.DATASET.normalize_sst,
+        "training": "eval",
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+    metric={
+        "rmse": ppsci.metric.FunctionalMetric(enso_metric.eval_rmse_func),
+    },
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用了自定义的评价指标分别是 MAEMSERMSEcorr_nino3.4_epochcorr_nino3.4_weighted_epoch

+

3.2.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/earthformer/earthformer_enso_train.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    seed=cfg.seed,
+    validator=validator,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.3 模型评估可视化

+

3.3.1 测试集上评估模型

+

构建模型的代码为:

+
examples/earthformer/earthformer_enso_train.py
model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

构建评估器的代码为:

+
examples/earthformer/earthformer_enso_train.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ENSODataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "in_stride": cfg.DATASET.in_stride,
+        "out_stride": cfg.DATASET.out_stride,
+        "train_samples_gap": cfg.DATASET.train_samples_gap,
+        "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+        "normalize_sst": cfg.DATASET.normalize_sst,
+        "training": "test",
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+    metric={
+        "rmse": ppsci.metric.FunctionalMetric(enso_metric.eval_rmse_func),
+    },
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

3.3.2 模型导出

+

构建模型的代码为:

+
examples/earthformer/earthformer_enso_train.py
# set model
+model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

实例化 ppsci.solver.Solver

+
examples/earthformer/earthformer_enso_train.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    pretrained_model_path=cfg.INFER.pretrained_model_path,
+)
+
+

构建模型输入格式并导出静态模型:

+
examples/earthformer/earthformer_enso_train.py
input_spec = [
+    {
+        key: InputSpec([1, 12, 24, 48, 1], "float32", name=key)
+        for key in model.input_keys
+    },
+]
+solver.export(input_spec, cfg.INFER.export_path)
+
+

InputSpec 函数中第一个设置模型输入尺寸,第二个参数设置输入数据类型,第三个设置输入数据的 Key.

+

3.3.3 模型推理

+

创建预测器:

+
examples/earthformer/earthformer_enso_train.py
import predictor
+
+predictor = predictor.EarthformerPredictor(cfg)
+
+

准备预测数据:

+
examples/earthformer/earthformer_enso_train.py
train_cmip = xr.open_dataset(cfg.INFER.data_path).transpose(
+    "year", "month", "lat", "lon"
+)
+# select longitudes
+lon = train_cmip.lon.values
+lon = lon[np.logical_and(lon >= 95, lon <= 330)]
+train_cmip = train_cmip.sel(lon=lon)
+data = train_cmip.sst.values
+data = enso_dataset.fold(data)
+
+idx_sst = enso_dataset.prepare_inputs_targets(
+    len_time=data.shape[0],
+    input_length=cfg.INFER.in_len,
+    input_gap=cfg.INFER.in_stride,
+    pred_shift=cfg.INFER.out_len * cfg.INFER.out_stride,
+    pred_length=cfg.INFER.out_len,
+    samples_gap=cfg.INFER.samples_gap,
+)
+data = data[idx_sst].astype("float32")
+
+sst_data = data[..., np.newaxis]
+idx = np.random.choice(len(data), None, False)
+in_seq = sst_data[idx, : cfg.INFER.in_len, ...]  # ( in_len, lat, lon, 1)
+in_seq = in_seq[np.newaxis, ...]
+
+

进行模型预测与预测值保存:

+
examples/earthformer/earthformer_enso_train.py
pred_data = predictor.predict(in_seq, cfg.INFER.batch_size)
+
+# save predict data
+save_path = osp.join(cfg.output_dir, "result_enso_pred.npy")
+np.save(save_path, pred_data)
+logger.info(f"Save output to {save_path}")
+
+

4. 云总降水量 vil 模型实现

+

4.1 数据集介绍

+

数据集采用了 EarthFormer 处理好的 SEVIR 数据集。

+

The Storm Event ImagRy(SEVIR) 数据集是由麻省理工林肯实验室和亚马逊收集并提供的。SEVIR 是一个经过注释、整理和时空对齐的数据集,包含 10,000 多个天气事件,每个事件由 384 千米 x 384 千米的图像序列组成,时间跨度为 4 小时。SEVIR 中的图像通过五种不同的数据类型进行采样和对齐:GOES-16 高级基线成像仪的三个通道 (C02、C09、C13)、NEXRAD 垂直液态水含量 (vil) 和 GOES-16 地球静止闪电成像 (GLM) 闪烁图。

+

SEVIR数据集的结构包括两部分:目录 (Catalog) 和数据文件 (Data File)。目录是一个 CSV 文件,其中包含描述事件元数据的行。数据文件是一组 HDF5 文件,包含特定传感器类型的事件。这些文件中的数据以 4D 张量形式存储,形状为 N x L x W x T,其中 N 是文件中的事件数,LxW 是图像大小,T 是图像序列中的时间步数。

+
+

SEVIR +

+
SEVIR 传感器类型说明
+
+

EarthFormer 采用 SEVIR 中的 NEXRAD 垂直液态水含量 (VIL) 作为降水预报的基准,即在 65 分钟的垂直综合液体背景下,预测未来 60 分钟的垂直综合液体。因此,分辨率为 13x384x384→12x384x384。

+

4.2 模型预训练

+

4.2.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数。

+

数据加载的代码如下:

+
examples/earthformer/earthformer_sevir_train.py
27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
# set train dataloader config
+train_dataloader_cfg = {
+    "dataset": {
+        "name": "SEVIRDataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "data_types": cfg.DATASET.data_types,
+        "seq_len": cfg.DATASET.seq_len,
+        "raw_seq_len": cfg.DATASET.raw_seq_len,
+        "sample_mode": cfg.DATASET.sample_mode,
+        "stride": cfg.DATASET.stride,
+        "batch_size": cfg.DATASET.batch_size,
+        "layout": cfg.DATASET.layout,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "split_mode": cfg.DATASET.split_mode,
+        "start_date": cfg.TRAIN.start_date,
+        "end_date": cfg.TRAIN.end_date,
+        "preprocess": cfg.DATASET.preprocess,
+        "rescale_method": cfg.DATASET.rescale_method,
+        "shuffle": True,
+        "verbose": False,
+        "training": True,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 ENSODataset,"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,设置的 batch_size 为 1,num_works 为 8。

+

定义监督约束的代码如下:

+
examples/earthformer/earthformer_sevir_train.py
61
+62
+63
+64
+65
+66
+67
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(sevir_metric.train_mse_func),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用自定义的损失函数 mse_loss

+

第三个参数是约束条件的名字,方便后续对其索引。此处命名为 Sup

+

4.2.2 模型构建

+

在该案例中,云总降水量模型基于 CuboidTransformer 网络模型实现,用 PaddleScience 代码表示如下:

+
examples/earthformer/earthformer_sevir_train.py
model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

定义模型的参数通过配置进行设置,如下:

+
examples/earthformer/conf/earthformer_sevir_pretrain.yaml
MODEL:
+  input_keys: ["input"]
+  output_keys: ["vil"]
+  input_shape: [13, 384, 384, 1]
+  target_shape: [12, 384, 384, 1]
+  base_units: 128
+  scale_alpha: 1.0
+
+  enc_depth: [1, 1]
+  dec_depth: [1, 1]
+  enc_use_inter_ffn: true
+  dec_use_inter_ffn: true
+  dec_hierarchical_pos_embed: false
+
+  downsample: 2
+  downsample_type: "patch_merge"
+  upsample_type: "upsample"
+
+  num_global_vectors: 8
+  use_dec_self_global: false
+  dec_self_update_global: true
+  use_dec_cross_global: false
+  use_global_vector_ffn: false
+  use_global_self_attn: true
+  separate_global_qkv: true
+  global_dim_ratio: 1
+
+  self_pattern: "axial"
+  cross_self_pattern: "axial"
+  cross_pattern: "cross_1x1"
+  dec_cross_last_n_frames: null
+
+  attn_drop: 0.1
+  proj_drop: 0.1
+  ffn_drop: 0.1
+  num_heads: 4
+
+  ffn_activation: "gelu"
+  gated_ffn: false
+  norm_layer: "layer_norm"
+  padding_type: "zeros"
+  pos_embed_type: "t+h+w"
+  use_relative_pos: true
+  self_attn_use_final_proj: true
+  dec_use_first_self_attn: false
+
+  z_init_method: "zeros"
+  initial_downsample_type: "stack_conv"
+  initial_downsample_activation: "leaky_relu"
+  initial_downsample_stack_conv_num_layers: 3
+  initial_downsample_stack_conv_dim_list: [16, 64, 128]
+  initial_downsample_stack_conv_downscale_list: [3, 2, 2]
+  initial_downsample_stack_conv_num_conv_list: [2, 2, 2]
+  checkpoint_level: 2
+
+  attn_linear_init_mode: "0"
+  ffn_linear_init_mode: "0"
+  conv_init_mode: "0"
+  down_up_linear_init_mode: "0"
+  norm_init_mode: "0"
+
+

其中,input_keysoutput_keys 分别代表网络模型输入、输出变量的名称。

+

4.2.3 学习率与优化器构建

+

本案例中使用的学习率方法为 Cosine,学习率大小设置为 1e-3。优化器使用 AdamW,并将参数进行分组,使用不同的 weight_decay,用 PaddleScience 代码表示如下:

+
examples/earthformer/earthformer_sevir_train.py
decay_parameters = get_parameter_names(model, [nn.LayerNorm])
+decay_parameters = [name for name in decay_parameters if "bias" not in name]
+optimizer_grouped_parameters = [
+    {
+        "params": [p for n, p in model.named_parameters() if n in decay_parameters],
+        "weight_decay": cfg.TRAIN.wd,
+    },
+    {
+        "params": [
+            p for n, p in model.named_parameters() if n not in decay_parameters
+        ],
+        "weight_decay": 0.0,
+    },
+]
+
+# init optimizer and lr scheduler
+lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(
+    **lr_scheduler_cfg,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    eta_min=cfg.TRAIN.min_lr_ratio * cfg.TRAIN.lr_scheduler.learning_rate,
+    warmup_epoch=int(0.2 * cfg.TRAIN.epochs),
+)()
+optimizer = paddle.optimizer.AdamW(
+    lr_scheduler, parameters=optimizer_grouped_parameters
+)
+
+

4.2.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/earthformer/earthformer_sevir_train.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "SEVIRDataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "data_types": cfg.DATASET.data_types,
+        "seq_len": cfg.DATASET.seq_len,
+        "raw_seq_len": cfg.DATASET.raw_seq_len,
+        "sample_mode": cfg.DATASET.sample_mode,
+        "stride": cfg.DATASET.stride,
+        "batch_size": cfg.DATASET.batch_size,
+        "layout": cfg.DATASET.layout,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "split_mode": cfg.DATASET.split_mode,
+        "start_date": cfg.TRAIN.end_date,
+        "end_date": cfg.EVAL.end_date,
+        "preprocess": cfg.DATASET.preprocess,
+        "rescale_method": cfg.DATASET.rescale_method,
+        "shuffle": False,
+        "verbose": False,
+        "training": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.MSELoss(),
+    metric={
+        "rmse": ppsci.metric.FunctionalMetric(
+            sevir_metric.eval_rmse_func(
+                out_len=cfg.DATASET.seq_len,
+                layout=cfg.DATASET.layout,
+                metrics_mode=cfg.EVAL.metrics_mode,
+                metrics_list=cfg.EVAL.metrics_list,
+                threshold_list=cfg.EVAL.threshold_list,
+            )
+        ),
+    },
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用了自定义的评价指标分别是 MAEMSEcsipodsucrbias,且后四个评价指标分别使用不同的阈值 [16,74,133,160,181,219]

+

4.2.5 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
examples/earthformer/earthformer_sevir_train.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    seed=cfg.seed,
+    validator=validator,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+# train model
+solver.train()
+
+

4.2.6 模型评估

+

由于目前 paddlescience 中的验证策略分为两类,一类是直接对验证数据集进行模型输出拼接,然后计算评价指标。另一类是按照每个 batch_size 计算评价指标,然后拼接,最后对所有结果求平均,该方法默认数据之间没有关联性。但是 SEVIR 数据集数据之间有关联性,所以不适用第二种方法;又由于 SEVIR 数据集量大,使用第一种方法验证显存需求大,因此验证 SEVIR 数据集使用的方法如下:

+
    +
  • 1.对一个 batch size 计算 hitsmissesfas 三个数据
  • +
  • 2.对数据集所有数据保存所有 batch 的三个值的累加和.
  • +
  • 3.对三个值的累加和计算 csipodsucrbias 四个指标。
  • +
+
examples/earthformer/earthformer_sevir_train.py
# evaluate after finished training
+metric = sevir_metric.eval_rmse_func(
+    out_len=cfg.DATASET.seq_len,
+    layout=cfg.DATASET.layout,
+    metrics_mode=cfg.EVAL.metrics_mode,
+    metrics_list=cfg.EVAL.metrics_list,
+    threshold_list=cfg.EVAL.threshold_list,
+)
+
+with solver.no_grad_context_manager(True):
+    for index, (input_, label, _) in enumerate(sup_validator.data_loader):
+        truefield = label["vil"].squeeze(0)
+        prefield = model(input_)["vil"].squeeze(0)
+        metric.sevir_score.update(prefield, truefield)
+
+metric_dict = metric.sevir_score.compute()
+print(metric_dict)
+
+

4.3 模型评估可视化

+

4.3.1 测试集上评估模型

+

构建模型的代码为:

+
examples/earthformer/earthformer_sevir_train.py
model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

构建评估器的代码为:

+
examples/earthformer/earthformer_sevir_train.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "SEVIRDataset",
+        "data_dir": cfg.FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.DATASET.label_keys,
+        "data_types": cfg.DATASET.data_types,
+        "seq_len": cfg.DATASET.seq_len,
+        "raw_seq_len": cfg.DATASET.raw_seq_len,
+        "sample_mode": cfg.DATASET.sample_mode,
+        "stride": cfg.DATASET.stride,
+        "batch_size": cfg.DATASET.batch_size,
+        "layout": cfg.DATASET.layout,
+        "in_len": cfg.DATASET.in_len,
+        "out_len": cfg.DATASET.out_len,
+        "split_mode": cfg.DATASET.split_mode,
+        "start_date": cfg.TEST.start_date,
+        "end_date": cfg.TEST.end_date,
+        "preprocess": cfg.DATASET.preprocess,
+        "rescale_method": cfg.DATASET.rescale_method,
+        "shuffle": False,
+        "verbose": False,
+        "training": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.MSELoss(),
+    metric={
+        "rmse": ppsci.metric.FunctionalMetric(
+            sevir_metric.eval_rmse_func(
+                out_len=cfg.DATASET.seq_len,
+                layout=cfg.DATASET.layout,
+                metrics_mode=cfg.EVAL.metrics_mode,
+                metrics_list=cfg.EVAL.metrics_list,
+                threshold_list=cfg.EVAL.threshold_list,
+            )
+        ),
+    },
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

模型评估:

+
examples/earthformer/earthformer_sevir_train.py
# evaluate
+metric = sevir_metric.eval_rmse_func(
+    out_len=cfg.DATASET.seq_len,
+    layout=cfg.DATASET.layout,
+    metrics_mode=cfg.EVAL.metrics_mode,
+    metrics_list=cfg.EVAL.metrics_list,
+    threshold_list=cfg.EVAL.threshold_list,
+)
+
+with solver.no_grad_context_manager(True):
+    for index, (input_, label, _) in enumerate(sup_validator.data_loader):
+        truefield = label["vil"].reshape([-1, *label["vil"].shape[2:]])
+        prefield = model(input_)["vil"].reshape([-1, *label["vil"].shape[2:]])
+        metric.sevir_score.update(prefield, truefield)
+
+metric_dict = metric.sevir_score.compute()
+print(metric_dict)
+
+

4.3.2 模型导出

+

构建模型的代码为:

+
examples/earthformer/earthformer_sevir_train.py
# set model
+model = ppsci.arch.CuboidTransformer(
+    **cfg.MODEL,
+)
+
+

实例化 ppsci.solver.Solver

+
examples/earthformer/earthformer_enso_train.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    pretrained_model_path=cfg.INFER.pretrained_model_path,
+)
+
+

构建模型输入格式并导出静态模型:

+
examples/earthformer/earthformer_sevir_train.py
input_spec = [
+    {
+        key: InputSpec([1, 13, 384, 384, 1], "float32", name=key)
+        for key in model.input_keys
+    },
+]
+solver.export(input_spec, cfg.INFER.export_path)
+
+

InputSpec 函数中第一个设置模型输入尺寸,第二个参数设置输入数据类型,第三个设置输入数据的 Key.

+

4.3.3 模型推理

+

创建预测器:

+
examples/earthformer/earthformer_sevir_train.py
predictor = predictor.EarthformerPredictor(cfg)
+
+

准备预测数据并进行对应模式的数据预处理:

+
examples/earthformer/earthformer_sevir_train.py
if cfg.INFER.rescale_method == "sevir":
+    scale_dict = sevir_dataset.PREPROCESS_SCALE_SEVIR
+    offset_dict = sevir_dataset.PREPROCESS_OFFSET_SEVIR
+elif cfg.INFER.rescale_method == "01":
+    scale_dict = sevir_dataset.PREPROCESS_SCALE_01
+    offset_dict = sevir_dataset.PREPROCESS_OFFSET_01
+else:
+    raise ValueError(f"Invalid rescale option: {cfg.INFER.rescale_method}.")
+
+# read h5 data
+h5data = h5py.File(cfg.INFER.data_path, "r")
+data = np.array(h5data[cfg.INFER.data_type]).transpose([0, 3, 1, 2])
+
+idx = np.random.choice(len(data), None, False)
+data = (
+    scale_dict[cfg.INFER.data_type] * data[idx] + offset_dict[cfg.INFER.data_type]
+)
+
+input_data = data[: cfg.INFER.in_len, ...]
+input_data = input_data.reshape(1, *input_data.shape, 1).astype(np.float32)
+
+

进行模型预测并可视化:

+
examples/earthformer/earthformer_sevir_train.py
pred_data = predictor.predict(input_data, cfg.INFER.batch_size)
+
+sevir_vis_seq.save_example_vis_results(
+    save_dir=cfg.INFER.sevir_vis_save,
+    save_prefix=f"data_{idx}",
+    in_seq=input_data,
+    target_seq=target_data,
+    pred_seq=pred_data,
+    layout=cfg.INFER.layout,
+    plot_stride=cfg.INFER.plot_stride,
+    label=cfg.INFER.logging_prefix,
+    interval_real_time=cfg.INFER.interval_real_time,
+)
+
+

5. 完整代码

+
examples/earthformer/earthformer_enso_train.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+from paddle import nn
+
+import examples.earthformer.enso_metric as enso_metric
+import ppsci
+from ppsci.data.dataset import enso_dataset
+from ppsci.utils import logger
+
+try:
+    import xarray as xr
+except ModuleNotFoundError:
+    raise ModuleNotFoundError("Please install xarray with `pip install xarray`.")
+
+
+def get_parameter_names(model, forbidden_layer_types):
+    result = []
+    for name, child in model.named_children():
+        result += [
+            f"{name}.{n}"
+            for n in get_parameter_names(child, forbidden_layer_types)
+            if not isinstance(child, tuple(forbidden_layer_types))
+        ]
+    # Add model specific parameters (defined with nn.Parameter) since they are not in any child.
+    result += list(model._parameters.keys())
+    return result
+
+
+def train(cfg: DictConfig):
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "ENSODataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "in_stride": cfg.DATASET.in_stride,
+            "out_stride": cfg.DATASET.out_stride,
+            "train_samples_gap": cfg.DATASET.train_samples_gap,
+            "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+            "normalize_sst": cfg.DATASET.normalize_sst,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 8,
+    }
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ENSODataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "in_stride": cfg.DATASET.in_stride,
+            "out_stride": cfg.DATASET.out_stride,
+            "train_samples_gap": cfg.DATASET.train_samples_gap,
+            "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+            "normalize_sst": cfg.DATASET.normalize_sst,
+            "training": "eval",
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+        metric={
+            "rmse": ppsci.metric.FunctionalMetric(enso_metric.eval_rmse_func),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    decay_parameters = get_parameter_names(model, [nn.LayerNorm])
+    decay_parameters = [name for name in decay_parameters if "bias" not in name]
+    optimizer_grouped_parameters = [
+        {
+            "params": [p for n, p in model.named_parameters() if n in decay_parameters],
+            "weight_decay": cfg.TRAIN.wd,
+        },
+        {
+            "params": [
+                p for n, p in model.named_parameters() if n not in decay_parameters
+            ],
+            "weight_decay": 0.0,
+        },
+    ]
+
+    # # init optimizer and lr scheduler
+    lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(
+        **lr_scheduler_cfg,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eta_min=cfg.TRAIN.min_lr_ratio * cfg.TRAIN.lr_scheduler.learning_rate,
+        warmup_epoch=int(0.2 * cfg.TRAIN.epochs),
+    )()
+    optimizer = paddle.optimizer.AdamW(
+        lr_scheduler, parameters=optimizer_grouped_parameters
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        seed=cfg.seed,
+        validator=validator,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ENSODataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "in_stride": cfg.DATASET.in_stride,
+            "out_stride": cfg.DATASET.out_stride,
+            "train_samples_gap": cfg.DATASET.train_samples_gap,
+            "eval_samples_gap": cfg.DATASET.eval_samples_gap,
+            "normalize_sst": cfg.DATASET.normalize_sst,
+            "training": "test",
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(enso_metric.train_mse_func),
+        metric={
+            "rmse": ppsci.metric.FunctionalMetric(enso_metric.eval_rmse_func),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate
+    solver.eval()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            key: InputSpec([1, 12, 24, 48, 1], "float32", name=key)
+            for key in model.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    import predictor
+
+    predictor = predictor.EarthformerPredictor(cfg)
+
+    train_cmip = xr.open_dataset(cfg.INFER.data_path).transpose(
+        "year", "month", "lat", "lon"
+    )
+    # select longitudes
+    lon = train_cmip.lon.values
+    lon = lon[np.logical_and(lon >= 95, lon <= 330)]
+    train_cmip = train_cmip.sel(lon=lon)
+    data = train_cmip.sst.values
+    data = enso_dataset.fold(data)
+
+    idx_sst = enso_dataset.prepare_inputs_targets(
+        len_time=data.shape[0],
+        input_length=cfg.INFER.in_len,
+        input_gap=cfg.INFER.in_stride,
+        pred_shift=cfg.INFER.out_len * cfg.INFER.out_stride,
+        pred_length=cfg.INFER.out_len,
+        samples_gap=cfg.INFER.samples_gap,
+    )
+    data = data[idx_sst].astype("float32")
+
+    sst_data = data[..., np.newaxis]
+    idx = np.random.choice(len(data), None, False)
+    in_seq = sst_data[idx, : cfg.INFER.in_len, ...]  # ( in_len, lat, lon, 1)
+    in_seq = in_seq[np.newaxis, ...]
+    target_seq = sst_data[idx, cfg.INFER.in_len :, ...]  # ( out_len, lat, lon, 1)
+    target_seq = target_seq[np.newaxis, ...]
+
+    pred_data = predictor.predict(in_seq, cfg.INFER.batch_size)
+
+    # save predict data
+    save_path = osp.join(cfg.output_dir, "result_enso_pred.npy")
+    np.save(save_path, pred_data)
+    logger.info(f"Save output to {save_path}")
+
+
+@hydra.main(
+    version_base=None,
+    config_path="./conf",
+    config_name="earthformer_enso_pretrain.yaml",
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
examples/earthformer/earthformer_sevir_train.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
import h5py
+import hydra
+import numpy as np
+import paddle
+import sevir_metric
+import sevir_vis_seq
+from omegaconf import DictConfig
+from paddle import nn
+
+import ppsci
+
+
+def get_parameter_names(model, forbidden_layer_types):
+    result = []
+    for name, child in model.named_children():
+        result += [
+            f"{name}.{n}"
+            for n in get_parameter_names(child, forbidden_layer_types)
+            if not isinstance(child, tuple(forbidden_layer_types))
+        ]
+    # Add model specific parameters (defined with nn.Parameter) since they are not in any child.
+    result += list(model._parameters.keys())
+    return result
+
+
+def train(cfg: DictConfig):
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "SEVIRDataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "data_types": cfg.DATASET.data_types,
+            "seq_len": cfg.DATASET.seq_len,
+            "raw_seq_len": cfg.DATASET.raw_seq_len,
+            "sample_mode": cfg.DATASET.sample_mode,
+            "stride": cfg.DATASET.stride,
+            "batch_size": cfg.DATASET.batch_size,
+            "layout": cfg.DATASET.layout,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "split_mode": cfg.DATASET.split_mode,
+            "start_date": cfg.TRAIN.start_date,
+            "end_date": cfg.TRAIN.end_date,
+            "preprocess": cfg.DATASET.preprocess,
+            "rescale_method": cfg.DATASET.rescale_method,
+            "shuffle": True,
+            "verbose": False,
+            "training": True,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 8,
+    }
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(sevir_metric.train_mse_func),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "SEVIRDataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "data_types": cfg.DATASET.data_types,
+            "seq_len": cfg.DATASET.seq_len,
+            "raw_seq_len": cfg.DATASET.raw_seq_len,
+            "sample_mode": cfg.DATASET.sample_mode,
+            "stride": cfg.DATASET.stride,
+            "batch_size": cfg.DATASET.batch_size,
+            "layout": cfg.DATASET.layout,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "split_mode": cfg.DATASET.split_mode,
+            "start_date": cfg.TRAIN.end_date,
+            "end_date": cfg.EVAL.end_date,
+            "preprocess": cfg.DATASET.preprocess,
+            "rescale_method": cfg.DATASET.rescale_method,
+            "shuffle": False,
+            "verbose": False,
+            "training": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.MSELoss(),
+        metric={
+            "rmse": ppsci.metric.FunctionalMetric(
+                sevir_metric.eval_rmse_func(
+                    out_len=cfg.DATASET.seq_len,
+                    layout=cfg.DATASET.layout,
+                    metrics_mode=cfg.EVAL.metrics_mode,
+                    metrics_list=cfg.EVAL.metrics_list,
+                    threshold_list=cfg.EVAL.threshold_list,
+                )
+            ),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    decay_parameters = get_parameter_names(model, [nn.LayerNorm])
+    decay_parameters = [name for name in decay_parameters if "bias" not in name]
+    optimizer_grouped_parameters = [
+        {
+            "params": [p for n, p in model.named_parameters() if n in decay_parameters],
+            "weight_decay": cfg.TRAIN.wd,
+        },
+        {
+            "params": [
+                p for n, p in model.named_parameters() if n not in decay_parameters
+            ],
+            "weight_decay": 0.0,
+        },
+    ]
+
+    # init optimizer and lr scheduler
+    lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(
+        **lr_scheduler_cfg,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eta_min=cfg.TRAIN.min_lr_ratio * cfg.TRAIN.lr_scheduler.learning_rate,
+        warmup_epoch=int(0.2 * cfg.TRAIN.epochs),
+    )()
+    optimizer = paddle.optimizer.AdamW(
+        lr_scheduler, parameters=optimizer_grouped_parameters
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        seed=cfg.seed,
+        validator=validator,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    metric = sevir_metric.eval_rmse_func(
+        out_len=cfg.DATASET.seq_len,
+        layout=cfg.DATASET.layout,
+        metrics_mode=cfg.EVAL.metrics_mode,
+        metrics_list=cfg.EVAL.metrics_list,
+        threshold_list=cfg.EVAL.threshold_list,
+    )
+
+    with solver.no_grad_context_manager(True):
+        for index, (input_, label, _) in enumerate(sup_validator.data_loader):
+            truefield = label["vil"].squeeze(0)
+            prefield = model(input_)["vil"].squeeze(0)
+            metric.sevir_score.update(prefield, truefield)
+
+    metric_dict = metric.sevir_score.compute()
+    print(metric_dict)
+
+
+def evaluate(cfg: DictConfig):
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "SEVIRDataset",
+            "data_dir": cfg.FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.DATASET.label_keys,
+            "data_types": cfg.DATASET.data_types,
+            "seq_len": cfg.DATASET.seq_len,
+            "raw_seq_len": cfg.DATASET.raw_seq_len,
+            "sample_mode": cfg.DATASET.sample_mode,
+            "stride": cfg.DATASET.stride,
+            "batch_size": cfg.DATASET.batch_size,
+            "layout": cfg.DATASET.layout,
+            "in_len": cfg.DATASET.in_len,
+            "out_len": cfg.DATASET.out_len,
+            "split_mode": cfg.DATASET.split_mode,
+            "start_date": cfg.TEST.start_date,
+            "end_date": cfg.TEST.end_date,
+            "preprocess": cfg.DATASET.preprocess,
+            "rescale_method": cfg.DATASET.rescale_method,
+            "shuffle": False,
+            "verbose": False,
+            "training": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.MSELoss(),
+        metric={
+            "rmse": ppsci.metric.FunctionalMetric(
+                sevir_metric.eval_rmse_func(
+                    out_len=cfg.DATASET.seq_len,
+                    layout=cfg.DATASET.layout,
+                    metrics_mode=cfg.EVAL.metrics_mode,
+                    metrics_list=cfg.EVAL.metrics_list,
+                    threshold_list=cfg.EVAL.threshold_list,
+                )
+            ),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate
+    metric = sevir_metric.eval_rmse_func(
+        out_len=cfg.DATASET.seq_len,
+        layout=cfg.DATASET.layout,
+        metrics_mode=cfg.EVAL.metrics_mode,
+        metrics_list=cfg.EVAL.metrics_list,
+        threshold_list=cfg.EVAL.threshold_list,
+    )
+
+    with solver.no_grad_context_manager(True):
+        for index, (input_, label, _) in enumerate(sup_validator.data_loader):
+            truefield = label["vil"].reshape([-1, *label["vil"].shape[2:]])
+            prefield = model(input_)["vil"].reshape([-1, *label["vil"].shape[2:]])
+            metric.sevir_score.update(prefield, truefield)
+
+    metric_dict = metric.sevir_score.compute()
+    print(metric_dict)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.CuboidTransformer(
+        **cfg.MODEL,
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            key: InputSpec([1, 13, 384, 384, 1], "float32", name=key)
+            for key in model.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    import predictor
+
+    from ppsci.data.dataset import sevir_dataset
+
+    predictor = predictor.EarthformerPredictor(cfg)
+
+    if cfg.INFER.rescale_method == "sevir":
+        scale_dict = sevir_dataset.PREPROCESS_SCALE_SEVIR
+        offset_dict = sevir_dataset.PREPROCESS_OFFSET_SEVIR
+    elif cfg.INFER.rescale_method == "01":
+        scale_dict = sevir_dataset.PREPROCESS_SCALE_01
+        offset_dict = sevir_dataset.PREPROCESS_OFFSET_01
+    else:
+        raise ValueError(f"Invalid rescale option: {cfg.INFER.rescale_method}.")
+
+    # read h5 data
+    h5data = h5py.File(cfg.INFER.data_path, "r")
+    data = np.array(h5data[cfg.INFER.data_type]).transpose([0, 3, 1, 2])
+
+    idx = np.random.choice(len(data), None, False)
+    data = (
+        scale_dict[cfg.INFER.data_type] * data[idx] + offset_dict[cfg.INFER.data_type]
+    )
+
+    input_data = data[: cfg.INFER.in_len, ...]
+    input_data = input_data.reshape(1, *input_data.shape, 1).astype(np.float32)
+    target_data = data[cfg.INFER.in_len : cfg.INFER.in_len + cfg.INFER.out_len, ...]
+    target_data = target_data.reshape(1, *target_data.shape, 1).astype(np.float32)
+
+    pred_data = predictor.predict(input_data, cfg.INFER.batch_size)
+
+    sevir_vis_seq.save_example_vis_results(
+        save_dir=cfg.INFER.sevir_vis_save,
+        save_prefix=f"data_{idx}",
+        in_seq=input_data,
+        target_seq=target_data,
+        pred_seq=pred_data,
+        layout=cfg.INFER.layout,
+        plot_stride=cfg.INFER.plot_stride,
+        label=cfg.INFER.logging_prefix,
+        interval_real_time=cfg.INFER.interval_real_time,
+    )
+
+
+@hydra.main(
+    version_base=None,
+    config_path="./conf",
+    config_name="earthformer_sevir_pretrain.yaml",
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

6. 结果展示

+

下图展示了云总降水量模型按照65分钟的输入数据,得到60分钟间隔的预测结果和真值结果。

+
+

SEVIR-predict +

+
SEVIR 中 vil 的预测结果("prediction")与真值结果("target")
+
+

说明:

+

Hit:TP, Miss:FN, False Alarm:FP

+

第一行: 输入数据;

+

第二行: 真值结果;

+

第三行: 预测结果;

+

第四行: 设定阈值为 74 情况下,TP、FN、FP 三种情况标记

+

第五行: 在所有阈值情况下,TP、FN、FP 三种情况标记

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/epnn/index.html b/zh/examples/epnn/index.html new file mode 100644 index 0000000000..3bce5d3a4a --- /dev/null +++ b/zh/examples/epnn/index.html @@ -0,0 +1,4386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EPNN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

EPNN

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstate-16-plas.dat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstress-16-plas.dat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstate-16-plas.dat --create-dirs -o ./datasets/dstate-16-plas.dat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstress-16-plas.dat --create-dirs -o ./datasets/dstress-16-plas.dat
+python epnn.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstate-16-plas.dat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstress-16-plas.dat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstate-16-plas.dat --create-dirs -o ./datasets/dstate-16-plas.dat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/epnn/dstress-16-plas.dat --create-dirs -o ./datasets/dstress-16-plas.dat
+python epnn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/epnn/epnn_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
epnn_pretrained.pdparamserror(total): 3.96903
error(error_elasto): 0.65328
error(error_plastic): 3.04176
error(error_stress): 0.27399
+

1. 背景简介

+

这里主要为复现 Elasto-Plastic Neural Network (EPNN) 的 Physics-Informed Neural Network (PINN) 代理模型。将这些物理嵌入神经网络的架构中,可以更有效地训练网络,同时使用更少的数据进行训练,同时增强对训练数据外加载制度的推断能力。EPNN 的架构是模型和材料无关的,即它可以适应各种弹塑性材料类型,包括地质材料和金属;并且实验数据可以直接用于训练网络。为了证明所提出架构的稳健性,我们将其一般框架应用于砂土的弹塑性行为。EPNN 在预测不同初始密度砂土的未观测应变控制加载路径方面优于常规神经网络架构。

+

2. 问题定义

+

在神经网络中,信息通过连接的神经元流动。神经网络中每个链接的“强度”是由一个可变的权重决定的:

+
\[ +z_l^{\mathrm{i}}=W_{k l}^{\mathrm{i}-1, \mathrm{i}} a_k^{\mathrm{i}-1}+b^{\mathrm{i}-1}, \quad k=1: N^{\mathrm{i}-1} \quad \text { or } \quad \mathbf{z}^{\mathrm{i}}=\mathbf{a}^{\mathrm{i}-1} \mathbf{W}^{\mathrm{i}-1, \mathrm{i}}+b^{\mathrm{i}-1} \mathbf{I} +\]
+

其中 \(b\) 是偏置项;\(N\) 为不同层中神经元数量;\(I\) 指的是所有元素都为 1 的单位向量。

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 EPNN 问题中,建立网络,用 PaddleScience 代码表示如下

+
node_sizes_state_plastic.extend(hl_nodes_plastic)
+node_sizes_stress.extend(hl_nodes_elasto)
+node_sizes_state_elasto.extend([state_y_output_size - 3])
+node_sizes_state_plastic.extend([state_y_output_size - 1])
+node_sizes_stress.extend([1])
+
+activation_elasto = "leaky_relu"
+activation_plastic = "leaky_relu"
+activations_elasto = [activation_elasto]
+activations_plastic = [activation_plastic]
+activations_elasto.extend([activation_elasto for ii in range(nhlayers)])
+activations_plastic.extend([activation_plastic for ii in range(NHLAYERS_PLASTIC)])
+activations_elasto.extend([activation_elasto])
+activations_plastic.extend([activation_plastic])
+drop_p = 0.0
+n_state_elasto = ppsci.arch.Epnn(
+    ("state_x",),
+    ("out_state_elasto",),
+    tuple(node_sizes_state_elasto),
+    tuple(activations_elasto),
+    drop_p,
+
+

EPNN 参数 input_keys 是输入字段名,output_keys 是输出字段名,node_sizes 是节点大小列表,activations 是激活函数字符串列表,drop_p 是节点丢弃概率。

+

3.2 数据生成

+

本案例涉及读取数据生成,如下所示

+
36
+37
+38
+39
+40
+41
(
+    input_dict_train,
+    label_dict_train,
+    input_dict_val,
+    label_dict_val,
+) = functions.get_data(cfg.DATASET_STATE, cfg.DATASET_STRESS, cfg.NTRAIN_SIZE)
+
+
        n_train = math.floor(self.train_p * self.x.shape[0])
+        n_cross_valid = math.floor(self.cross_valid_p * self.x.shape[0])
+        n_test = math.floor(self.test_p * self.x.shape[0])
+        self.x_train = self.x[shuffled_indices[0:n_train]]
+        self.y_train = self.y[shuffled_indices[0:n_train]]
+        self.x_valid = self.x[shuffled_indices[n_train : n_train + n_cross_valid]]
+        self.y_valid = self.y[shuffled_indices[n_train : n_train + n_cross_valid]]
+        self.x_test = self.x[
+            shuffled_indices[n_train + n_cross_valid : n_train + n_cross_valid + n_test]
+        ]
+        self.y_test = self.y[
+            shuffled_indices[n_train + n_cross_valid : n_train + n_cross_valid + n_test]
+        ]
+
+
+def get_data(dataset_state, dataset_stress, ntrain_size):
+
+

这里使用 Data 读取文件构造数据类,然后使用 get_shuffled_data 混淆数据,然后计算需要获取的混淆数据数量 itrain,最后使用 get 获取每组 itrain 数量的 10 组数据。

+

3.3 约束构建

+

设置训练数据集和损失计算函数,返回字段,代码如下所示:

+
63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
output_keys = [
+    "state_x",
+    "state_y",
+    "stress_x",
+    "stress_y",
+    "out_state_elasto",
+    "out_state_plastic",
+    "out_stress",
+]
+sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_train,
+            "label": label_dict_train,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.train_loss_func),
+    {key: (lambda out, k=key: out[k]) for key in output_keys},
+    name="sup_train",
+)
+constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,配置中 “dataset” 字段表示使用的训练数据集信息,其各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 "NamedArrayDataset" 表示顺序读取的数据集;
  2. +
  3. input: 输入数据集;
  4. +
  5. label: 标签数据集;
  6. +
+

第二个参数是损失函数,此处使用自定义函数 train_loss_func

+

第三个参数是方程表达式,用于描述如何计算约束目标,计算后的值将会按照指定名称存入输出列表中,从而保证 loss 计算时可以使用这些值。

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+

3.4 评估器构建

+

与约束同理,本问题使用 ppsci.validate.SupervisedValidator 构建评估器,参数含义也与约束构建类似,唯一的区别是评价指标 metric。代码如下所示:

+
sup_validator_pde = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_val,
+            "label": label_dict_val,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.eval_loss_func),
+    {key: (lambda out, k=key: out[k]) for key in output_keys},
+    metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+    name="sup_valid",
+)
+validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数,此处我们按实验经验,使用 10000 轮训练轮数。iters_per_epoch 为 1。

+
epochs: 10000
+iters_per_epoch: 1
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并配合使用机器学习中常用的 ExponentialDecay 学习率调整策略。

+

由于使用多个模型,需要设置多个优化器,对 EPNN 网络部分,需要设置 Adam 优化器。

+
    ("out_state_plastic",),
+    tuple(node_sizes_state_plastic),
+    tuple(activations_plastic),
+    drop_p,
+)
+n_stress = ppsci.arch.Epnn(
+    ("state_x_f",),
+    ("out_stress",),
+    tuple(node_sizes_stress),
+    tuple(activations_elasto),
+
+

然后对增加的 gkratio 参数,需要再设置优化器。

+
    )
+    return (n_state_elasto, n_state_plastic, n_stress)
+
+
+def get_optimizer_list(model_list, cfg):
+    optimizer_list = []
+    lr_list = [0.001, 0.001, 0.01]
+    for i, model in enumerate(model_list):
+
+

优化器按顺序优化,代码汇总为:

+
        ("out_state_plastic",),
+        tuple(node_sizes_state_plastic),
+        tuple(activations_plastic),
+        drop_p,
+    )
+    n_stress = ppsci.arch.Epnn(
+        ("state_x_f",),
+        ("out_stress",),
+        tuple(node_sizes_stress),
+        tuple(activations_elasto),
+        drop_p,
+    )
+    return (n_state_elasto, n_state_plastic, n_stress)
+
+
+def get_optimizer_list(model_list, cfg):
+    optimizer_list = []
+    lr_list = [0.001, 0.001, 0.01]
+    for i, model in enumerate(model_list):
+
+

3.7 自定义 loss

+

由于本问题包含无监督学习,数据中不存在标签数据,loss 根据模型返回数据计算得到,因此需要自定义 loss。方法为先定义相关函数,再将函数名作为参数传给 FunctionalLossFunctionalMetric

+

需要注意自定义 loss 函数的输入输出参数需要与 PaddleScience 中如 MSE 等其他函数保持一致,即输入为模型输出 output_dict 等字典变量,loss 函数输出为 loss 值 paddle.Tensor

+

相关的自定义 loss 函数使用 MAELoss 计算,代码为

+
        )
+    }
+
+
+def train_loss_func(output_dict, *args) -> paddle.Tensor:
+    """For model calculation of loss in model.train().
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): The output dict.
+
+    Returns:
+        paddle.Tensor: Loss value.
+    """
+
+

3.8 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
solver = ppsci.solver.Solver(
+    model_list_obj,
+    constraint_pde,
+    cfg.output_dir,
+    optimizer_list,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    validator=validator_pde,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+
+

模型训练时设置 eval_during_train 为 True,将在每次训练后评估。

+
eval_during_train: true
+
+

最后启动训练即可:

+
solver.train()
+
+

4. 完整代码

+
epnn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/meghbali/ANNElastoplasticity
+"""
+
+from os import path as osp
+
+import functions
+import hydra
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    (
+        input_dict_train,
+        label_dict_train,
+        input_dict_val,
+        label_dict_val,
+    ) = functions.get_data(cfg.DATASET_STATE, cfg.DATASET_STRESS, cfg.NTRAIN_SIZE)
+    model_list = functions.get_model_list(
+        cfg.MODEL.ihlayers,
+        cfg.MODEL.ineurons,
+        input_dict_train["state_x"][0].shape[1],
+        input_dict_train["state_y"][0].shape[1],
+        input_dict_train["stress_x"][0].shape[1],
+    )
+    optimizer_list = functions.get_optimizer_list(model_list, cfg)
+    model_state_elasto, model_state_plastic, model_stress = model_list
+    model_list_obj = ppsci.arch.ModelList(model_list)
+
+    def _transform_in_stress(_in):
+        return functions.transform_in_stress(
+            _in, model_state_elasto, "out_state_elasto"
+        )
+
+    model_state_elasto.register_input_transform(functions.transform_in)
+    model_state_plastic.register_input_transform(functions.transform_in)
+    model_stress.register_input_transform(_transform_in_stress)
+    model_stress.register_output_transform(functions.transform_out)
+
+    output_keys = [
+        "state_x",
+        "state_y",
+        "stress_x",
+        "stress_y",
+        "out_state_elasto",
+        "out_state_plastic",
+        "out_stress",
+    ]
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_train,
+                "label": label_dict_train,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func),
+        {key: (lambda out, k=key: out[k]) for key in output_keys},
+        name="sup_train",
+    )
+    constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.eval_loss_func),
+        {key: (lambda out, k=key: out[k]) for key in output_keys},
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list_obj,
+        constraint_pde,
+        cfg.output_dir,
+        optimizer_list,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        validator=validator_pde,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # train model
+    solver.train()
+    functions.plotting(cfg.output_dir)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    (
+        input_dict_train,
+        _,
+        input_dict_val,
+        label_dict_val,
+    ) = functions.get_data(cfg.DATASET_STATE, cfg.DATASET_STRESS, cfg.NTRAIN_SIZE)
+    model_list = functions.get_model_list(
+        cfg.MODEL.ihlayers,
+        cfg.MODEL.ineurons,
+        input_dict_train["state_x"][0].shape[1],
+        input_dict_train["state_y"][0].shape[1],
+        input_dict_train["stress_x"][0].shape[1],
+    )
+    model_state_elasto, model_state_plastic, model_stress = model_list
+    model_list_obj = ppsci.arch.ModelList(model_list)
+
+    def _transform_in_stress(_in):
+        return functions.transform_in_stress(
+            _in, model_state_elasto, "out_state_elasto"
+        )
+
+    model_state_elasto.register_input_transform(functions.transform_in)
+    model_state_plastic.register_input_transform(functions.transform_in)
+    model_stress.register_input_transform(_transform_in_stress)
+    model_stress.register_output_transform(functions.transform_out)
+
+    output_keys = [
+        "state_x",
+        "state_y",
+        "stress_x",
+        "stress_y",
+        "out_state_elasto",
+        "out_state_plastic",
+        "out_stress",
+    ]
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.eval_loss_func),
+        {key: (lambda out, k=key: out[k]) for key in output_keys},
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+    functions.OUTPUT_DIR = cfg.output_dir
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list_obj,
+        output_dir=cfg.output_dir,
+        validator=validator_pde,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate
+    solver.eval()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="epnn.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

EPNN 案例针对 epoch=10000 的参数配置进行了实验,结果返回 Loss 为 0.00471。

+

下图分别为不同 epoch 的 Loss, Training error, Cross validation error 图形:

+
+

loss_trend +

+
训练 loss 图形
+
+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/euler_beam/index.html b/zh/examples/euler_beam/index.html new file mode 100644 index 0000000000..3ff8dfd36f --- /dev/null +++ b/zh/examples/euler_beam/index.html @@ -0,0 +1,4456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Euler_Beam - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Euler Beam

+
+
+
+
python euler_beam.py
+
+
+
+
python euler_beam.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/euler_beam/euler_beam_pretrained.pdparams
+
+
+
+
python euler_beam.py mode=export
+
+
+
+
python euler_beam.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
euler_beam_pretrained.pdparamsloss(L2Rel_Metric): 0.00000
L2Rel.u(L2Rel_Metric): 0.00058
+

1. 问题定义

+

Euler Beam 公式:

+
\[ +\dfrac{\partial^{4} u}{\partial x^{4}} + 1 = 0, x \in [0, 1] +\]
+

边界条件:

+
\[ +u''(1)=0, u'''(1)=0 +\]
+

狄利克雷条件:

+
\[ +u(0)=0 +\]
+

诺依曼边界条件:

+
\[ +u'(0)=0 +\]
+

2. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

2.1 模型构建

+

在 Euler Beam 问题中,每一个已知的坐标点 \(x\) 都有对应的待求解的未知量 \(u\) +,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \(x\)\(u\) 的映射函数 \(f: \mathbb{R}^1 \to \mathbb{R}^1\) ,即:

+
\[ +u = f(x) +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

其中,用于初始化模型的参数通过配置文件进行配置:

+
38
+39
+40
+41
+42
+43
# model settings
+MODEL:
+  input_keys: ["x"]
+  output_keys: ["u"]
+  num_layers: 3
+  hidden_size: 20
+
+

接着通过指定 MLP 的层数、神经元个数,我们就实例化出了一个拥有 3 层隐藏神经元,每层神经元数为 20 的神经网络模型 model

+

2.2 方程构建

+

Euler Beam 的方程构建可以直接使用 PaddleScience 内置的 Biharmonic,指定该类的参数 dim 为 1,q 为 -1,D 为1。

+
# set equation(s)
+equation = {"biharmonic": ppsci.equation.Biharmonic(dim=1, q=cfg.q, D=cfg.D)}
+
+

2.3 计算域构建

+

本文中 Euler Beam 问题作用在以 (0.0, 1.0) 的一维区域上, +因此可以直接使用 PaddleScience 内置的空间几何 Interval 作为计算域。

+
# set geometry
+geom = {"interval": ppsci.geometry.Interval(0, 1)}
+
+

2.4 约束构建

+

在本案例中,我们使用了两个约束条件在计算域中指导模型的训练分别是作用于采样点上的方程约束和作用于边界点上的约束。

+

在定义约束之前,需要给每一种约束指定采样点个数,表示每一种约束在其对应计算域内采样数据的数量,以及通用的采样配置。

+
45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
# training settings
+TRAIN:
+  epochs: 10000
+  iters_per_epoch: 1
+  save_freq: 1000
+  eval_during_train: true
+  eval_freq: 1000
+  learning_rate: 1.0e-3
+  batch_size:
+    pde: 100
+    bc: 4
+
+

2.4.1 内部点约束

+

以作用在内部点上的 InteriorConstraint 为例,代码如下:

+
33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
# set dataloader config
+dataloader_cfg = {
+    "dataset": "IterableNamedArrayDataset",
+    "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+}
+# set constraint
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["biharmonic"].equations,
+    {"biharmonic": 0},
+    geom["interval"],
+    {**dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.pde},
+    ppsci.loss.MSELoss(),
+    random="Hammersley",
+    name="EQ",
+)
+
+

2.4.2 边界约束

+

同理,我们还需要构建边界的约束。但与构建 InteriorConstraint 约束不同的是,由于作用区域是边界,因此我们使用 BoundaryConstraint 类,代码如下:

+
48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
bc = ppsci.constraint.BoundaryConstraint(
+    {
+        "u0": lambda d: d["u"][0:1],
+        "u__x": lambda d: jacobian(d["u"], d["x"])[1:2],
+        "u__x__x": lambda d: hessian(d["u"], d["x"])[2:3],
+        "u__x__x__x": lambda d: jacobian(hessian(d["u"], d["x"]), d["x"])[3:4],
+    },
+    {"u0": 0, "u__x": 0, "u__x__x": 0, "u__x__x__x": 0},
+    geom["interval"],
+    {**dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc},
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    name="BC",
+)
+
+

2.5 超参数设定

+

接下来我们需要在配置文件中指定训练轮数,此处我们按实验经验,使用一万轮训练轮数,评估间隔为一千轮。

+
45
+46
+47
+48
+49
+50
+51
# training settings
+TRAIN:
+  epochs: 10000
+  iters_per_epoch: 1
+  save_freq: 1000
+  eval_during_train: true
+  eval_freq: 1000
+
+

2.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

2.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
l2_rel_metric = ppsci.validate.GeometryValidator(
+    {"u": lambda out: out["u"]},
+    {"u": u_solution_func},
+    geom["interval"],
+    {
+        "dataset": "IterableNamedArrayDataset",
+        "total_size": cfg.EVAL.total_size,
+    },
+    ppsci.loss.MSELoss(),
+    evenly=True,
+    metric={"L2Rel": ppsci.metric.L2Rel()},
+    name="L2Rel_Metric",
+)
+validator = {l2_rel_metric.name: l2_rel_metric}
+
+

2.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文中的输出数据是一个曲线图,因此我们只需要将评估的输出数据保存成 png 文件即可。代码如下:

+
# set visualizer(optional)
+visu_points = geom["interval"].sample_interior(cfg.EVAL.total_size, evenly=True)
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visu_points,
+        ("x",),
+        {
+            "u_label": lambda d: u_solution_func(d),
+            "u_pred": lambda d: d["u"],
+        },
+        num_timestamps=1,
+        prefix="result_u",
+    )
+}
+
+

2.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    seed=cfg.seed,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    to_static=cfg.to_static,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

3. 完整代码

+
euler_beam.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hydra
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import hessian
+from ppsci.autodiff import jacobian
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set geometry
+    geom = {"interval": ppsci.geometry.Interval(0, 1)}
+
+    # set equation(s)
+    equation = {"biharmonic": ppsci.equation.Biharmonic(dim=1, q=cfg.q, D=cfg.D)}
+
+    # set dataloader config
+    dataloader_cfg = {
+        "dataset": "IterableNamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+    # set constraint
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["biharmonic"].equations,
+        {"biharmonic": 0},
+        geom["interval"],
+        {**dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.pde},
+        ppsci.loss.MSELoss(),
+        random="Hammersley",
+        name="EQ",
+    )
+    bc = ppsci.constraint.BoundaryConstraint(
+        {
+            "u0": lambda d: d["u"][0:1],
+            "u__x": lambda d: jacobian(d["u"], d["x"])[1:2],
+            "u__x__x": lambda d: hessian(d["u"], d["x"])[2:3],
+            "u__x__x__x": lambda d: jacobian(hessian(d["u"], d["x"]), d["x"])[3:4],
+        },
+        {"u0": 0, "u__x": 0, "u__x__x": 0, "u__x__x__x": 0},
+        geom["interval"],
+        {**dataloader_cfg, "batch_size": cfg.TRAIN.batch_size.bc},
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        name="BC",
+    )
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        bc.name: bc,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    def u_solution_func(out):
+        """compute ground truth for u as label data"""
+        x = out["x"]
+        return -(x**4) / 24 + x**3 / 6 - x**2 / 4
+
+    l2_rel_metric = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["interval"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": cfg.EVAL.total_size,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="L2Rel_Metric",
+    )
+    validator = {l2_rel_metric.name: l2_rel_metric}
+
+    # set visualizer(optional)
+    visu_points = geom["interval"].sample_interior(cfg.EVAL.total_size, evenly=True)
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerScatter1D(
+            visu_points,
+            ("x",),
+            {
+                "u_label": lambda d: u_solution_func(d),
+                "u_pred": lambda d: d["u"],
+            },
+            num_timestamps=1,
+            prefix="result_u",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        to_static=cfg.to_static,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set geometry
+    geom = {"interval": ppsci.geometry.Interval(0, 1)}
+
+    # set equation(s)
+    equation = {"biharmonic": ppsci.equation.Biharmonic(dim=1, q=cfg.q, D=cfg.D)}
+
+    # set validator
+    def u_solution_func(out):
+        """compute ground truth for u as label data"""
+        x = out["x"]
+        return -(x**4) / 24 + x**3 / 6 - x**2 / 4
+
+    l2_rel_metric = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["interval"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": cfg.EVAL.total_size,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="L2Rel_Metric",
+    )
+    validator = {l2_rel_metric.name: l2_rel_metric}
+
+    # set visualizer(optional)
+    visu_points = geom["interval"].sample_interior(cfg.EVAL.total_size, evenly=True)
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerScatter1D(
+            visu_points,
+            ("x",),
+            {
+                "u_label": lambda d: u_solution_func(d),
+                "u_pred": lambda d: d["u"],
+            },
+            num_timestamps=1,
+            prefix="result_u",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        None,
+        cfg.output_dir,
+        None,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        to_static=cfg.to_static,
+    )
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    geom = {"interval": ppsci.geometry.Interval(0, 1)}
+    input_dict = geom["interval"].sample_interior(cfg.INFER.total_size, evenly=True)
+
+    output_dict = predictor.predict({"x": input_dict["x"]}, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    def u_solution_func(out):
+        """compute ground truth for u as label data"""
+        x = out["x"]
+        return -(x**4) / 24 + x**3 / 6 - x**2 / 4
+
+    ppsci.visualize.save_plot_from_1d_dict(
+        "./euler_beam_pred",
+        {**input_dict, **output_dict, "u_label": u_solution_func(input_dict)},
+        ("x",),
+        ("u", "u_label"),
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="euler_beam.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

4. 结果展示

+

使用训练得到的模型对上述计算域中均匀取的共 NPOINT_TOTAL 个点 \(x_i\) 进行预测,预测结果如下所示。图像中横坐标为 \(x\),纵坐标为对应的预测结果 \(u\)

+
+

euler_beam +

+
模型预测结果
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/fourcastnet/index.html b/zh/examples/fourcastnet/index.html new file mode 100644 index 0000000000..f09f1f5b53 --- /dev/null +++ b/zh/examples/fourcastnet/index.html @@ -0,0 +1,7266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FourCastNet - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

FourCastNet

+

AI Studio快速体验

+

开始训练、评估前,请先下载数据集

+
+
+
+
# 风速预训练模型
+python train_pretrain.py
+# 风速微调模型
+python train_finetune.py
+# 降水模型训练
+python train_precip.py
+
+
+
+
# 风速预训练模型评估
+python train_pretrain.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/fourcastnet/pretrain.pdparams
+# 风速微调模型评估
+python train_finetune.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/fourcastnet/finetune.pdparams
+# 降水量模型评估
+python train_precip.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/fourcastnet/precip.pdparams WIND_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/fourcastnet/finetune.pdparams
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
模型变量名称ACC/RMSE(6h)ACC/RMSE(30h)ACC/RMSE(60h)ACC/RMSE(120h)ACC/RMSE(192h)
风速模型U100.991/0.5670.963/1.1300.891/1.9300.645/3.4380.371/4.915
+ + + + + + + + + + + + + + + + + + + + + +
模型变量名称ACC/RMSE(6h)ACC/RMSE(12h)ACC/RMSE(24h)ACC/RMSE(36h)
降水量模型TP0.808/1.3900.760/1.5400.668/1.6900.590/1.920
+

1. 背景简介

+

在天气预报任务中,有基于物理信息驱动和数据驱动两种方法实现天气预报。基于物理信息驱动的方法,往往依赖物理方程,通过建模大气变量之间的物理关系实现天气预报。例如在 IFS 模型中,使用了分布在 50 多个垂直高度上共 150 多个大气变量实现天气的预测。基于数据驱动的方法不依赖物理方程,但是需要大量的训练数据,一般将神经网络看作一个黑盒结构,训练网络学习输入数据与输出数据之间的函数关系,实现给定输入条件下对于输出数据的预测。FourCastNet是一种基于数据驱动方法的气象预报算法,它使用自适应傅里叶神经算子(AFNO)进行训练和预测。该算法专注于预测两大气象变量:距离地球表面10米处的风速和6小时总降水量,以对极端天气、自然灾害等进行预警。相比于 IFS 模型,它仅仅使用了 5 个垂直高度上共 20 个大气变量,具有大气变量输入个数少,推理理速度快的特点。

+

2. 模型原理

+

本章节仅对 FourCastNet 的模型原理进行简单地介绍,详细的理论推导请阅读 FourCastNet: A Global Data-driven High-resolution Weather Model using Adaptive Fourier Neural Operators

+

FourCastNet 的网络模型使用了 AFNO 网络,该网络此前常用于图像分割任务。这个网络通过 FNO 弥补了 ViT 网络的缺点,使用傅立叶变换完成不同 token 信息交互,显著减少了高分辨率下 ViT 中 self-attention 的计算量。关于 AFNOFNOVIT 的相关原理也请阅读对应论文。

+

模型的总体结构如图所示:

+
+

fourcastnet-arch +

+
FourCastNet 网络模型
+
+

FourCastNet论文中训练了风速模型和降水量模型,接下来将介绍这两个模型的训练、推理过程。

+

2.1 风速模型的训练、推理过程

+

模型的训练过程主要分为两个步骤:模型预训练、模型微调。

+

模型预训练阶段是基于随机初始化的网络权重对模型进行训练,如下图所示,其中 \(X(k)\) 表示第 \(k\) 时刻的大气数据,\(X(k+1)\) 表示第 \(k+1\) 时刻模型预测的大气数据,\(X_{true}(k+1)\) 表示第 \(k+1\) 时刻的真实大气数据。最后网络模型预测的输出和真值计算 L2 损失函数。

+
+

fourcastnet-pretraining +

+
风速模型预训练
+
+

模型训练的第二个阶段是模型微调,这个阶段的训练主要是为了提高模型在中长期天气预报的精度。具体地,当模型输入 \(k\) 时刻的数据,预测了 \(k+1\) 时刻的数据后,再将其重新作为输入预测 \(k+2\) 时刻的数据,以连续预测两个时刻的训练方式,提高模型长时预测能力。

+
+

fourcastnet-finetuning +

+
风速模型微调
+
+

在推理阶段,给定 \(k\) 时刻的数据,可以通过不断迭代,得到 \(k+1\)\(k+2\)\(k+3\) 等时刻的预测结果。

+
+

fourcastnet-inference +

+
风速模型推理
+
+

2.2 降水量模型的训练、推理过程

+

降水量模型的训练依赖于风速模型,如下图所示,使用 \(k\) 时刻的大气变量数据 \(X(k)\) 输入训练好的风速模型,得到预测的 \(k+1\) 时刻的大气变量数据 \(X(k+1)\)。降水量模型以 \(X(k+1)\) 为输入,输出为 \(k+1\) 时刻的降水量预测结果 \(p(k+1)\)。模型训练时 \(p(k+1)\) 与真值数据 \(p_{true}(k+1)\) 计算 L2 损失函数约束网络训练。

+
+

precip-training +

+
降水量模型训练
+
+

需要注意的是在降水量模型的训练过程中,风速模型的参数处于冻结状态,不参与优化器参数更新过程。

+

在推理阶段,给定 \(k\) 时刻的数据,可以通过不断迭代,利用风速模型得到 \(k+1\)\(k+2\)\(k+3\) 等时刻的大气变量预测结果,作为降水量模型的输入,预测对应时刻的降水量。

+
+

precip-inference +

+
降水量模型推理
+
+

3. 风速模型实现

+

接下来开始讲解如何基于 PaddleScience 代码,实现 FourCastNet 风速模型的训练与推理。关于该案例中的其余细节请参考 API文档

+
+Info +

由于完整复现需要 5+TB 的存储空间和 64 卡的训练资源,因此如果仅仅是为了学习 FourCastNet 的算法原理,建议对一小部分训练数据集进行训练,以减小学习成本。

+
+

3.1 数据集介绍

+

数据集采用了 FourCastNet 中处理好的 ERA5 数据集。该数据集的分辨率大小为 0.25 度,每个变量的数据尺寸为 \(720 \times 1440\),其中单个数据点代表的实际距离为 30km 左右。FourCastNet 使用了 1979-2018 年的数据,根据年份划分为了训练集、验证集、测试集,划分结果如下:

+ + + + + + + + + + + + + + + + + + + + + +
数据集年份
训练集1979-2015
验证集2016-2017
测试集2018
+

该数据集可以从此处下载。

+

模型训练使用了分布在 5 个压力层上的 20 个大气变量,如下表所示,

+
+

fourcastnet-vars +

+
20 个大气变量
+
+

其中 \(T\)\(U\)\(V\)\(Z\)\(RH\) 分别代表指定垂直高度上的温度、纬向风速、经向风速、位势和相对湿度;\(U_{10}\)\(V_{10}\)\(T_{2m}\) 则代表距离地面 10 米的纬向风速、经向风速和距离地面 2 米的温度。\(sp\) 代表地面气压,\(mslp\) 代表平均海平面气压。\(TCWV\) 代表整层气柱水汽总量。

+

对每天 24 个小时的数据间隔 6 小时采样,得到 0.00h/6.00h/12.00h/18.00h 时刻全球 20 个大气变量的数据,使用这样的数据进行模型的训练与推理。即输入0.00h 时刻的 20 个大气变量的数据,模型输出预测得到的 6.00h 时刻的 20 个大气变量的数据。

+

3.2 模型预训练

+

首先展示代码中定义的各个参数变量,每个参数的具体含义会在下面使用到时进行解释。

+
examples/fourcastnet/conf/fourcastnet_pretrain.yaml
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
# set training hyper-parameters
+IMG_H: 720
+IMG_W: 1440
+# FourCastNet use 20 atmospheric variable,their index in the dataset is from 0 to 19.
+# The variable name is 'u10', 'v10', 't2m', 'sp', 'msl', 't850', 'u1000', 'v1000', 'z000',
+# 'u850', 'v850', 'z850',  'u500', 'v500', 'z500', 't500', 'z50', 'r500', 'r850', 'tcwv'.
+# You can obtain detailed information about each variable from
+# https://cds.climate.copernicus.eu/cdsapp#!/search?text=era5&type=dataset
+VARS_CHANNEL: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+USE_SAMPLED_DATA: false
+
+# set train data path
+TRAIN_FILE_PATH: ./datasets/era5/train
+DATA_MEAN_PATH: ./datasets/era5/stat/global_means.npy
+DATA_STD_PATH: ./datasets/era5/stat/global_stds.npy
+DATA_TIME_MEAN_PATH: ./datasets/era5/stat/time_means.npy
+
+# set evaluate data path
+VALID_FILE_PATH: ./datasets/era5/test
+
+

3.2.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,首先介绍数据预处理部分,代码如下:

+
examples/fourcastnet/train_pretrain.py
46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
data_mean, data_std = fourcast_utils.get_mean_std(
+    cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+)
+data_time_mean = fourcast_utils.get_time_mean(
+    cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+)
+data_time_mean_normalize = np.expand_dims(
+    (data_time_mean[0] - data_mean) / data_std, 0
+)
+# set train transforms
+transforms = [
+    {"SqueezeData": {}},
+    {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+    {"Normalize": {"mean": data_mean, "std": data_std}},
+]
+
+

数据预处理部分总共包含 3 个预处理方法,分别是:

+
    +
  1. SqueezeData: 对训练数据的维度进行压缩,如果输入数据的维度为 4,则将第 0 维和第 1 维的数据压缩到一起,最终将输入数据的维度变换为 3。
  2. +
  3. CropData: 从训练数据中裁剪指定位置的数据。因为 ERA5 数据集中的原始数据尺寸为 \(721 \times 1440\),本案例根据原始论文设置,将训练数据裁剪为 \(720 \times 1440\)
  4. +
  5. Normalize: 根据训练数据集上的均值、方差对数据进行归一化处理。
  6. +
+

由于完整复现 FourCastNet 需要 5TB+ 的存储空间和 64 卡的 GPU 资源,需要的存储资源比较多,因此有以下两种训练方式(实验证明两种训练方式的损失函数收敛曲线基本一致,当存储资源比较有限时,可以使用方式 b)。

+

方式 a: 当存储资源充足时,可以不对数据进行划分,每个节点都有一份完整5TB+的训练数据,然后直接启动训练程序进行训练,此时每个节点上的数据随机抽取自完整训练数据。本方式的训练数据的加载是使用全局 shuffle 的方式进行,如下图所示。

+
+

fourcastnet-vars +

+
全局 shuffle
+
+

这种方式下,数据加载的代码如下:

+
examples/fourcastnet/train_pretrain.py
64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
train_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.afno.input_keys,
+        "label_keys": cfg.MODEL.afno.output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "transforms": transforms,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 ERA5Dataset,"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,设置的 batch_size 为 1,num_works 为 8。

+

方式 b:在存储资源有限时,需要将数据集均匀切分至每个节点上,本案例提供了随机采样数据的程序,可以执行 ppsci/fourcastnet/sample_data.py,可以根据需要进行修改。本案例默认使用方式 a, 因此使用方式 b 进行模型训练时需要手动将 USE_SAMPLED_DATA 设置为 True。本方式的训练数据的加载是使用局部 shuffle 的方式进行,如下图所示,首先将训练数据平均切分至 8 个节点上,训练时每个节点的数据随机抽取自被切分到的数据上,在这一情况下,每个节点需要约 1.2TB 的存储空间,相比于方式 a,方式 b 大大减小了对存储空间的依赖。

+
+

fourcastnet-vars +

+
局部 shuffle
+
+

这种方式下,数据加载的代码如下:

+
examples/fourcastnet/train_pretrain.py
82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
NUM_GPUS_PER_NODE = 8
+train_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5SampledDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.afno.input_keys,
+        "label_keys": cfg.MODEL.afno.output_keys,
+    },
+    "sampler": {
+        "name": "DistributedBatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+        "num_replicas": NUM_GPUS_PER_NODE,
+        "rank": dist.get_rank() % NUM_GPUS_PER_NODE,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 ERA5SampledDataset,"sampler" 字段定义了使用的 Sampler 类名为 DistributedBatchSampler,设置的 batch_size 为 1,num_works 为 8。

+

当不需要完整复现 FourCastNet 时,直接使用本案例的默认设置(方式 a)即可,

+

定义监督约束的代码如下:

+
examples/fourcastnet/train_pretrain.py
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用 L2RelLoss

+

第三个参数是约束条件的名字,方便后续对其索引。此处命名为 "Sup"。

+

3.2.2 模型构建

+

在该案例中,风速模型基于 AFNONet 网络模型,用 PaddleScience 代码表示如下:

+
examples/fourcastnet/train_pretrain.py
# set model
+model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+
+

网络模型的参数通过配置文件进行设置如下:

+
examples/fourcastnet/conf/fourcastnet_pretrain.yaml
48
+49
+50
+51
+52
# model settings
+MODEL:
+  afno:
+    input_keys: ["input"]
+    output_keys: ["output"]
+
+

其中,input_keysoutput_keys 分别代表网络模型输入、输出变量的名称。

+

3.2.3 学习率与优化器构建

+

本案例中使用的学习率方法为 Cosine,学习率大小设置为 5e-4。优化器使用 Adam,用 PaddleScience 代码表示如下:

+
examples/fourcastnet/train_pretrain.py
# init optimizer and lr scheduler
+lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+lr_scheduler_cfg.update({"iters_per_epoch": ITERS_PER_EPOCH})
+lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(**lr_scheduler_cfg)()
+
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.2.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/fourcastnet/train_pretrain.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.afno.input_keys,
+        "label_keys": cfg.MODEL.afno.output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "transforms": transforms,
+        "training": False,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+# set validator
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    metric={
+        "MAE": ppsci.metric.MAE(keep_batch=True),
+        "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+            num_lat=cfg.IMG_H,
+            std=data_std,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+        "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+            num_lat=cfg.IMG_H,
+            mean=data_time_mean_normalize,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+    },
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用了 3 个评价指标分别是 MAELatitudeWeightedRMSELatitudeWeightedACC

+

3.2.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/fourcastnet/train_pretrain.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=True,
+    seed=cfg.seed,
+    validator=validator,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.3 模型微调

+

上文介绍了如何对风速模型进行预训练,在本节中将介绍如何利用预训练的模型进行微调。因为风速模型预训练的步骤与微调的步骤基本相似,因此本节在两者的重复部分不再介绍,而仅仅介绍模型微调特有的部分。首先将代码中定义的各个参数变量展示如下,每个参数的具体含义会在下面使用到时进行解释。

+
examples/fourcastnet/conf/fourcastnet_finetune.yaml
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
# set training hyper-parameters
+IMG_H: 720
+IMG_W: 1440
+# FourCastNet use 20 atmospheric variable,their index in the dataset is from 0 to 19.
+# The variable name is 'u10', 'v10', 't2m', 'sp', 'msl', 't850', 'u1000', 'v1000', 'z000',
+# 'u850', 'v850', 'z850',  'u500', 'v500', 'z500', 't500', 'z50', 'r500', 'r850', 'tcwv'.
+# You can obtain detailed information about each variable from
+# https://cds.climate.copernicus.eu/cdsapp#!/search?text=era5&type=dataset
+VARS_CHANNEL: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+
+# set train data path
+TRAIN_FILE_PATH: ./datasets/era5/train
+DATA_MEAN_PATH: ./datasets/era5/stat/global_means.npy
+DATA_STD_PATH: ./datasets/era5/stat/global_stds.npy
+DATA_TIME_MEAN_PATH: ./datasets/era5/stat/time_means.npy
+
+# set evaluate data path
+VALID_FILE_PATH: ./datasets/era5/test
+
+# set test data path
+TEST_FILE_PATH: ./datasets/era5/out_of_sample/2018.h5
+
+

微调模型的程序新增了 num_timestamps 参数,用于控制模型微调训练时迭代的时间步的个数。这个参数首先会在数据加载的设置中用到,用于设置数据集产生的真值的时间步大小,代码如下:

+
examples/fourcastnet/train_finetune.py
# set train dataloader config
+train_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.afno.input_keys,
+        "label_keys": output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "num_label_timestamps": cfg.TRAIN.num_timestamps,
+        "transforms": transforms,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

num_timestamps 参数通过配置文件进行设置,如下:

+
examples/fourcastnet/conf/fourcastnet_finetune.yaml
num_timestamps: 2
+
+

另外,与预训练不同的是,微调的模型构建也需要设置 num_timestamps 参数,用于控制模型输出的预测结果的时间步大小,代码如下:

+
examples/fourcastnet/train_finetune.py
# set model
+model_cfg = dict(cfg.MODEL.afno)
+model_cfg.update(
+    {"output_keys": output_keys, "num_timestamps": cfg.TRAIN.num_timestamps}
+)
+
+

训练微调模型的程序中增加了在测试集上评估模型性能的代码和可视化代码,接下来将对这两部分进行详细介绍。

+

3.3.1 测试集上评估模型

+

根据论文中的设置,在测试集上进行模型评估时,num_timestamps 通过配置文件设置的为 32,相邻的两个测试样本的间隔为 8。

+
examples/fourcastnet/conf/fourcastnet_finetune.yaml
70
+71
+72
# evaluation settings
+EVAL:
+  num_timestamps: 32
+
+

构建模型的代码为:

+
examples/fourcastnet/train_finetune.py
# set model
+model_cfg = dict(cfg.MODEL.afno)
+model_cfg.update(
+    {"output_keys": output_keys, "num_timestamps": cfg.EVAL.num_timestamps}
+)
+model = ppsci.arch.AFNONet(**model_cfg)
+
+

构建评估器的代码为:

+
examples/fourcastnet/train_finetune.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.TEST_FILE_PATH,
+        "input_keys": cfg.MODEL.afno.input_keys,
+        "label_keys": output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "transforms": transforms,
+        "num_label_timestamps": cfg.EVAL.num_timestamps,
+        "training": False,
+        "stride": 8,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+# set metirc
+metric = {
+    "MAE": ppsci.metric.MAE(keep_batch=True),
+    "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+        num_lat=cfg.IMG_H,
+        std=data_std,
+        keep_batch=True,
+        variable_dict={"u10": 0, "v10": 1},
+    ),
+    "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+        num_lat=cfg.IMG_H,
+        mean=data_time_mean_normalize,
+        keep_batch=True,
+        variable_dict={"u10": 0, "v10": 1},
+    ),
+}
+
+# set validator for testing
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    metric=metric,
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

3.3.2 可视化器构建

+

风速模型使用自回归的方式进行推理,需要首先设置模型推理的输入数据,代码如下:

+
examples/fourcastnet/train_finetune.py
# set visualizer data
+DATE_STRINGS = ("2018-09-08 00:00:00",)
+vis_data = get_vis_data(
+    cfg.TEST_FILE_PATH,
+    DATE_STRINGS,
+    cfg.EVAL.num_timestamps,
+    cfg.VARS_CHANNEL,
+    cfg.IMG_H,
+    data_mean,
+    data_std,
+)
+
+
examples/fourcastnet/train_finetune.py
30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
def get_vis_data(
+    file_path: str,
+    date_strings: Tuple[str, ...],
+    num_timestamps: int,
+    vars_channel: Tuple[int, ...],
+    img_h: int,
+    data_mean: np.ndarray,
+    data_std: np.ndarray,
+):
+    _file = h5py.File(file_path, "r")["fields"]
+    data = []
+    for date_str in date_strings:
+        hours_since_jan_01_epoch = fourcast_utils.date_to_hours(date_str)
+        ic = int(hours_since_jan_01_epoch / 6)
+        data.append(_file[ic : ic + num_timestamps + 1, vars_channel, 0:img_h])
+    data = np.asarray(data)
+
+    vis_data = {"input": (data[:, 0] - data_mean) / data_std}
+    for t in range(num_timestamps):
+        hour = (t + 1) * 6
+        data_t = data[:, t + 1]
+        wind_data = []
+        for i in range(data_t.shape[0]):
+            wind_data.append((data_t[i][0] ** 2 + data_t[i][1] ** 2) ** 0.5)
+        vis_data[f"target_{hour}h"] = np.asarray(wind_data)
+    return vis_data
+
+

以上的代码中会根据设置的时间参数 DATE_STRINGS 读取对应的数据用于模型的输入,另外 get_vis_datas 函数内还读取了对应时刻的真值数据,这些数据也将可视化出来,方便与模型的预测结果进行对比。

+

由于模型对风速的纬向和经向分开预测,因此需要把这两个方向上的风速合成为真正的风速,代码如下:

+
examples/fourcastnet/train_finetune.py
def output_wind_func(d, var_name, data_mean, data_std):
+    output = (d[var_name] * data_std) + data_mean
+    wind_data = []
+    for i in range(output.shape[0]):
+        wind_data.append((output[i][0] ** 2 + output[i][1] ** 2) ** 0.5)
+    return paddle.to_tensor(wind_data, paddle.get_default_dtype())
+
+vis_output_expr = {}
+for i in range(cfg.EVAL.num_timestamps):
+    hour = (i + 1) * 6
+    vis_output_expr[f"output_{hour}h"] = functools.partial(
+        output_wind_func,
+        var_name=f"output_{i}",
+        data_mean=paddle.to_tensor(data_mean, paddle.get_default_dtype()),
+        data_std=paddle.to_tensor(data_std, paddle.get_default_dtype()),
+    )
+    vis_output_expr[f"target_{hour}h"] = lambda d, hour=hour: d[f"target_{hour}h"]
+
+

最后,构建可视化器的代码如下:

+
examples/fourcastnet/train_finetune.py
# set visualizer
+visualizer = {
+    "visualize_wind": ppsci.visualize.VisualizerWeather(
+        vis_data,
+        vis_output_expr,
+        xticks=np.linspace(0, 1439, 13),
+        xticklabels=[str(i) for i in range(360, -1, -30)],
+        yticks=np.linspace(0, 719, 7),
+        yticklabels=[str(i) for i in range(90, -91, -30)],
+        vmin=0,
+        vmax=25,
+        colorbar_label="m\s",
+        batch_size=cfg.EVAL.batch_size,
+        num_timestamps=cfg.EVAL.num_timestamps,
+        prefix="wind",
+    )
+}
+
+

以上构建好的模型、评估器、可视化器将会传递给 ppsci.solver.Solver 用于在测试集上评估性能和进行可视化。

+
examples/fourcastnet/train_finetune.py
solver = ppsci.solver.Solver(
+    model,
+    output_dir=cfg.output_dir,
+    validator=validator,
+    visualizer=visualizer,
+    pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+solver.eval()
+# visualize prediction from pretrained_model_path
+solver.visualize()
+
+

4. 降水量模型实现

+

首先展示代码中定义的各个参数变量,每个参数的具体含义会在下面使用到时进行解释。

+
examples/fourcastnet/conf/fourcastnet_precip.yaml
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
# set training hyper-parameters
+IMG_H: 720
+IMG_W: 1440
+# FourCastNet use 20 atmospheric variable,their index in the dataset is from 0 to 19.
+# The variable name is 'u10', 'v10', 't2m', 'sp', 'msl', 't850', 'u1000', 'v1000', 'z000',
+# 'u850', 'v850', 'z850',  'u500', 'v500', 'z500', 't500', 'z50', 'r500', 'r850', 'tcwv'.
+# You can obtain detailed information about each variable from
+# https://cds.climate.copernicus.eu/cdsapp#!/search?text=era5&type=dataset
+VARS_CHANNEL: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
+
+# set train data path
+WIND_TRAIN_FILE_PATH: ./datasets/era5/train
+WIND_MEAN_PATH: ./datasets/era5/stat/global_means.npy
+WIND_STD_PATH: ./datasets/era5/stat/global_stds.npy
+WIND_TIME_MEAN_PATH: ./datasets/era5/stat/time_means.npy
+
+TRAIN_FILE_PATH: ./datasets/era5/precip/train
+TIME_MEAN_PATH: ./datasets/era5/stat/precip/time_means.npy
+
+# set evaluate data path
+WIND_VALID_FILE_PATH: ./datasets/era5/test
+VALID_FILE_PATH: ./datasets/era5/precip/test
+
+# set test data path
+WIND_TEST_FILE_PATH: ./datasets/era5/out_of_sample/2018.h5
+TEST_FILE_PATH: ./datasets/era5/precip/out_of_sample/2018.h5
+
+# set wind model path
+WIND_MODEL_PATH: outputs_fourcastnet_finetune/checkpoints/latest
+
+

4.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,首先介绍数据预处理部分,代码如下:

+
examples/fourcastnet/train_precip.py
66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
wind_data_mean, wind_data_std = fourcast_utils.get_mean_std(
+    cfg.WIND_MEAN_PATH, cfg.WIND_STD_PATH, cfg.VARS_CHANNEL
+)
+data_time_mean = fourcast_utils.get_time_mean(
+    cfg.TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W
+)
+
+# set train transforms
+transforms = [
+    {"SqueezeData": {}},
+    {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+    {
+        "Normalize": {
+            "mean": wind_data_mean,
+            "std": wind_data_std,
+            "apply_keys": ("input",),
+        }
+    },
+    {"Log1p": {"scale": 1e-5, "apply_keys": ("label",)}},
+]
+
+

数据预处理部分总共包含 4 个预处理方法,分别是:

+
    +
  1. SqueezeData: 对训练数据的维度进行压缩,如果输入数据的维度为 4,则将第 0 维和第 1 维的数据压缩到一起,最终将输入数据的维度变换为 3。
  2. +
  3. CropData: 从训练数据中裁剪指定位置的数据。因为 ERA5 数据集中的原始数据尺寸为 \(721 \times 1440\),本案例根据原始论文设置,将训练数据尺寸裁剪为 \(720 \times 1440\)
  4. +
  5. Normalize: 根据训练数据集上的均值、方差对数据进行归一化处理,这里通过 apply_keys 字段设置了该预处理方法仅仅应用到输入数据上。
  6. +
  7. Log1p: 将数据映射到对数空间,这里通过 apply_keys 字段设置了该预处理方法仅仅应用到真值数据上。
  8. +
+

数据加载的代码如下:

+
examples/fourcastnet/train_precip.py
# set train dataloader config
+train_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.WIND_TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.precip.input_keys,
+        "label_keys": cfg.MODEL.precip.output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "precip_file_path": cfg.TRAIN_FILE_PATH,
+        "transforms": transforms,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 8,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 ERA5Dataset,"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,设置的 batch_size 为 1,num_works 为 8。

+

定义监督约束的代码如下:

+
examples/fourcastnet/train_precip.py
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用 L2RelLoss

+

第三个参数是约束条件的名字,方便后续对其索引。此处命名为 "Sup"。

+

4.2 模型构建

+

在该案例中,需要首先定义风速模型的网络结构并加载训练好的参数,然后定义降水量模型,用 PaddleScience 代码表示如下:

+
examples/fourcastnet/train_precip.py
# set model
+wind_model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+ppsci.utils.save_load.load_pretrain(wind_model, path=cfg.WIND_MODEL_PATH)
+model_cfg = dict(cfg.MODEL.precip)
+model_cfg.update({"wind_model": wind_model})
+model = ppsci.arch.PrecipNet(**model_cfg)
+
+

定义模型的参数通过配置进行设置,如下:

+
examples/fourcastnet/conf/fourcastnet_precip.yaml
58
+59
+60
+61
+62
+63
+64
+65
# model settings
+MODEL:
+  afno:
+    input_keys: ["input"]
+    output_keys: ["output"]
+  precip:
+    input_keys: ["input"]
+    output_keys: ["output"]
+
+

其中,input_keysoutput_keys 分别代表网络模型输入、输出变量的名称。

+

4.3 学习率与优化器构建

+

本案例中使用的学习率方法为 Cosine,学习率大小设置为 2.5e-4。优化器使用 Adam,用 PaddleScience 代码表示如下:

+
examples/fourcastnet/train_precip.py
# init optimizer and lr scheduler
+lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+lr_scheduler_cfg.update({"iters_per_epoch": ITERS_PER_EPOCH})
+lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(**lr_scheduler_cfg)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

4.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/fourcastnet/train_precip.py
# set eval dataloader config
+eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.WIND_VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.precip.input_keys,
+        "label_keys": cfg.MODEL.precip.output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "precip_file_path": cfg.VALID_FILE_PATH,
+        "transforms": transforms,
+        "training": False,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+
+# set metric
+metric = {
+    "MAE": ppsci.metric.MAE(keep_batch=True),
+    "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+        num_lat=cfg.IMG_H, keep_batch=True, unlog=True
+    ),
+    "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+        num_lat=cfg.IMG_H, mean=data_time_mean, keep_batch=True, unlog=True
+    ),
+}
+
+# set validator
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    metric=metric,
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用了 3 个评价指标分别是 MAELatitudeWeightedRMSELatitudeWeightedACC

+

4.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/fourcastnet/train_precip.py
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=True,
+    validator=validator,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

4.6 测试集上评估模型

+

根据论文中的设置,在测试集上进行模型评估时,num_timestamps 设置为 6,相邻的两个测试样本的间隔为 8。

+

构建模型的代码为:

+
examples/fourcastnet/train_precip.py
# set model for testing
+wind_model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+ppsci.utils.save_load.load_pretrain(wind_model, path=cfg.WIND_MODEL_PATH)
+model_cfg = dict(cfg.MODEL.precip)
+model_cfg.update(
+    {
+        "output_keys": output_keys,
+        "num_timestamps": cfg.EVAL.num_timestamps,
+        "wind_model": wind_model,
+    }
+)
+model = ppsci.arch.PrecipNet(**model_cfg)
+
+

构建评估器的代码为:

+
examples/fourcastnet/train_precip.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "ERA5Dataset",
+        "file_path": cfg.WIND_TEST_FILE_PATH,
+        "input_keys": cfg.MODEL.precip.input_keys,
+        "label_keys": output_keys,
+        "vars_channel": cfg.VARS_CHANNEL,
+        "precip_file_path": cfg.TEST_FILE_PATH,
+        "num_label_timestamps": cfg.EVAL.num_timestamps,
+        "stride": 8,
+        "transforms": transforms,
+        "training": False,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+}
+# set metirc
+metric = {
+    "MAE": ppsci.metric.MAE(keep_batch=True),
+    "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+        num_lat=cfg.IMG_H, keep_batch=True, unlog=True
+    ),
+    "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+        num_lat=cfg.IMG_H, mean=data_time_mean, keep_batch=True, unlog=True
+    ),
+}
+
+# set validator for testing
+sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    metric=metric,
+    name="Sup_Validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

4.7 可视化器构建

+

降水量模型使用自回归的方式进行推理,需要首先设置模型推理的输入数据,代码如下:

+
examples/fourcastnet/train_precip.py
# set set visualizer data
+DATE_STRINGS = ("2018-04-04 00:00:00",)
+vis_data = get_vis_data(
+    cfg.WIND_TEST_FILE_PATH,
+    cfg.TEST_FILE_PATH,
+    DATE_STRINGS,
+    cfg.EVAL.num_timestamps,
+    cfg.VARS_CHANNEL,
+    cfg.IMG_H,
+    wind_data_mean,
+    wind_data_std,
+)
+
+
examples/fourcastnet/train_precip.py
30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
def get_vis_data(
+    wind_file_path: str,
+    file_path: str,
+    date_strings: Tuple[str, ...],
+    num_timestamps: int,
+    vars_channel: Tuple[int, ...],
+    img_h: int,
+    data_mean: np.ndarray,
+    data_std: np.ndarray,
+):
+    __wind_file = h5py.File(wind_file_path, "r")["fields"]
+    _file = h5py.File(file_path, "r")["tp"]
+    wind_data = []
+    data = []
+    for date_str in date_strings:
+        hours_since_jan_01_epoch = fourcast_utils.date_to_hours(date_str)
+        ic = int(hours_since_jan_01_epoch / 6)
+        wind_data.append(__wind_file[ic, vars_channel, 0:img_h])
+        data.append(_file[ic + 1 : ic + num_timestamps + 1, 0:img_h])
+    wind_data = np.asarray(wind_data)
+    data = np.asarray(data)
+
+    vis_data = {"input": (wind_data - data_mean) / data_std}
+    for t in range(num_timestamps):
+        hour = (t + 1) * 6
+        data_t = data[:, t]
+        vis_data[f"target_{hour}h"] = np.asarray(data_t)
+    return vis_data
+
+

以上的代码中会根据设置的时间参数 DATE_STRINGS 读取对应的数据用于模型的输入,另外 get_vis_datas 函数内还读取了对应时刻的真值数据,这些数据也将可视化出来,方便与模型的预测结果进行对比。

+

由于模型对降水量进行了对数处理,因此需要将模型结果重新映射回线性空间,代码如下:

+
examples/fourcastnet/train_precip.py
def output_precip_func(d, var_name):
+    output = 1e-2 * paddle.expm1(d[var_name][0])
+    return output
+
+visu_output_expr = {}
+for i in range(cfg.EVAL.num_timestamps):
+    hour = (i + 1) * 6
+    visu_output_expr[f"output_{hour}h"] = functools.partial(
+        output_precip_func,
+        var_name=f"output_{i}",
+    )
+    visu_output_expr[f"target_{hour}h"] = (
+        lambda d, hour=hour: d[f"target_{hour}h"] * 1000
+    )
+
+

最后,构建可视化器的代码如下:

+
examples/fourcastnet/train_precip.py
# set visualizer
+visualizer = {
+    "visualize_precip": ppsci.visualize.VisualizerWeather(
+        vis_data,
+        visu_output_expr,
+        xticks=np.linspace(0, 1439, 13),
+        xticklabels=[str(i) for i in range(360, -1, -30)],
+        yticks=np.linspace(0, 719, 7),
+        yticklabels=[str(i) for i in range(90, -91, -30)],
+        vmin=0.001,
+        vmax=130,
+        colorbar_label="mm",
+        log_norm=True,
+        batch_size=cfg.EVAL.batch_size,
+        num_timestamps=cfg.EVAL.num_timestamps,
+        prefix="precip",
+    )
+}
+
+

以上构建好的模型、评估器、可视化器将会传递给 ppsci.solver.Solver 用于在测试集上评估性能和进行可视化。

+
examples/fourcastnet/train_precip.py
solver = ppsci.solver.Solver(
+    model,
+    output_dir=cfg.output_dir,
+    validator=validator,
+    visualizer=visualizer,
+    pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+solver.eval()
+# visualize prediction
+solver.visualize()
+
+

5. 完整代码

+
examples/fourcastnet/train_pretrain.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle.distributed as dist
+from omegaconf import DictConfig
+
+import examples.fourcastnet.utils as fourcast_utils
+import ppsci
+from ppsci.utils import logger
+
+
+def get_data_stat(cfg: DictConfig):
+    data_mean, data_std = fourcast_utils.get_mean_std(
+        cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+    )
+    data_time_mean_normalize = np.expand_dims(
+        (data_time_mean[0] - data_mean) / data_std, 0
+    )
+    return data_mean, data_std, data_time_mean_normalize
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    data_mean, data_std = fourcast_utils.get_mean_std(
+        cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+    )
+    data_time_mean_normalize = np.expand_dims(
+        (data_time_mean[0] - data_mean) / data_std, 0
+    )
+    # set train transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {"Normalize": {"mean": data_mean, "std": data_std}},
+    ]
+
+    # set train dataloader config
+    if not cfg.USE_SAMPLED_DATA:
+        train_dataloader_cfg = {
+            "dataset": {
+                "name": "ERA5Dataset",
+                "file_path": cfg.TRAIN_FILE_PATH,
+                "input_keys": cfg.MODEL.afno.input_keys,
+                "label_keys": cfg.MODEL.afno.output_keys,
+                "vars_channel": cfg.VARS_CHANNEL,
+                "transforms": transforms,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": True,
+                "shuffle": True,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "num_workers": 8,
+        }
+    else:
+        NUM_GPUS_PER_NODE = 8
+        train_dataloader_cfg = {
+            "dataset": {
+                "name": "ERA5SampledDataset",
+                "file_path": cfg.TRAIN_FILE_PATH,
+                "input_keys": cfg.MODEL.afno.input_keys,
+                "label_keys": cfg.MODEL.afno.output_keys,
+            },
+            "sampler": {
+                "name": "DistributedBatchSampler",
+                "drop_last": True,
+                "shuffle": True,
+                "num_replicas": NUM_GPUS_PER_NODE,
+                "rank": dist.get_rank() % NUM_GPUS_PER_NODE,
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "num_workers": 8,
+        }
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.afno.input_keys,
+            "label_keys": cfg.MODEL.afno.output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "transforms": transforms,
+            "training": False,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    # set validator
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric={
+            "MAE": ppsci.metric.MAE(keep_batch=True),
+            "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+                num_lat=cfg.IMG_H,
+                std=data_std,
+                keep_batch=True,
+                variable_dict={"u10": 0, "v10": 1},
+            ),
+            "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+                num_lat=cfg.IMG_H,
+                mean=data_time_mean_normalize,
+                keep_batch=True,
+                variable_dict={"u10": 0, "v10": 1},
+            ),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set model
+    model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+
+    # init optimizer and lr scheduler
+    lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+    lr_scheduler_cfg.update({"iters_per_epoch": ITERS_PER_EPOCH})
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(**lr_scheduler_cfg)()
+
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=True,
+        seed=cfg.seed,
+        validator=validator,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    data_mean, data_std = fourcast_utils.get_mean_std(
+        cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+    )
+    data_time_mean_normalize = np.expand_dims(
+        (data_time_mean[0] - data_mean) / data_std, 0
+    )
+    # set train transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {"Normalize": {"mean": data_mean, "std": data_std}},
+    ]
+
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.afno.input_keys,
+            "label_keys": cfg.MODEL.afno.output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "transforms": transforms,
+            "training": False,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    # set validator
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric={
+            "MAE": ppsci.metric.MAE(keep_batch=True),
+            "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+                num_lat=cfg.IMG_H,
+                std=data_std,
+                keep_batch=True,
+                variable_dict={"u10": 0, "v10": 1},
+            ),
+            "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+                num_lat=cfg.IMG_H,
+                mean=data_time_mean_normalize,
+                keep_batch=True,
+                variable_dict={"u10": 0, "v10": 1},
+            ),
+        },
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set model
+    model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate
+    solver.eval()
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="fourcastnet_pretrain.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
examples/fourcastnet/train_finetune.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import functools
+from os import path as osp
+from typing import Tuple
+
+import h5py
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import examples.fourcastnet.utils as fourcast_utils
+import ppsci
+from ppsci.utils import logger
+
+
+def get_vis_data(
+    file_path: str,
+    date_strings: Tuple[str, ...],
+    num_timestamps: int,
+    vars_channel: Tuple[int, ...],
+    img_h: int,
+    data_mean: np.ndarray,
+    data_std: np.ndarray,
+):
+    _file = h5py.File(file_path, "r")["fields"]
+    data = []
+    for date_str in date_strings:
+        hours_since_jan_01_epoch = fourcast_utils.date_to_hours(date_str)
+        ic = int(hours_since_jan_01_epoch / 6)
+        data.append(_file[ic : ic + num_timestamps + 1, vars_channel, 0:img_h])
+    data = np.asarray(data)
+
+    vis_data = {"input": (data[:, 0] - data_mean) / data_std}
+    for t in range(num_timestamps):
+        hour = (t + 1) * 6
+        data_t = data[:, t + 1]
+        wind_data = []
+        for i in range(data_t.shape[0]):
+            wind_data.append((data_t[i][0] ** 2 + data_t[i][1] ** 2) ** 0.5)
+        vis_data[f"target_{hour}h"] = np.asarray(wind_data)
+    return vis_data
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set training hyper-parameters
+    output_keys = tuple(f"output_{i}" for i in range(cfg.TRAIN.num_timestamps))
+
+    data_mean, data_std = fourcast_utils.get_mean_std(
+        cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+    )
+    data_time_mean_normalize = np.expand_dims(
+        (data_time_mean[0] - data_mean) / data_std, 0
+    )
+
+    # set transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {"Normalize": {"mean": data_mean, "std": data_std}},
+    ]
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.afno.input_keys,
+            "label_keys": output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "num_label_timestamps": cfg.TRAIN.num_timestamps,
+            "transforms": transforms,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 8,
+    }
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.afno.input_keys,
+            "label_keys": output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "transforms": transforms,
+            "num_label_timestamps": cfg.TRAIN.num_timestamps,
+            "training": False,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    # set metric
+    metric = {
+        "MAE": ppsci.metric.MAE(keep_batch=True),
+        "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+            num_lat=cfg.IMG_H,
+            std=data_std,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+        "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+            num_lat=cfg.IMG_H,
+            mean=data_time_mean_normalize,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+    }
+
+    # set validator
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric=metric,
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set model
+    model_cfg = dict(cfg.MODEL.afno)
+    model_cfg.update(
+        {"output_keys": output_keys, "num_timestamps": cfg.TRAIN.num_timestamps}
+    )
+
+    model = ppsci.arch.AFNONet(**model_cfg)
+
+    # init optimizer and lr scheduler
+    lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+    lr_scheduler_cfg.update({"iters_per_epoch": ITERS_PER_EPOCH})
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(**lr_scheduler_cfg)()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=True,
+        validator=validator,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set testing hyper-parameters
+    output_keys = tuple(f"output_{i}" for i in range(cfg.EVAL.num_timestamps))
+
+    data_mean, data_std = fourcast_utils.get_mean_std(
+        cfg.DATA_MEAN_PATH, cfg.DATA_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.DATA_TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W, cfg.VARS_CHANNEL
+    )
+    data_time_mean_normalize = np.expand_dims(
+        (data_time_mean[0] - data_mean) / data_std, 0
+    )
+
+    # set transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {"Normalize": {"mean": data_mean, "std": data_std}},
+    ]
+
+    # set model
+    model_cfg = dict(cfg.MODEL.afno)
+    model_cfg.update(
+        {"output_keys": output_keys, "num_timestamps": cfg.EVAL.num_timestamps}
+    )
+    model = ppsci.arch.AFNONet(**model_cfg)
+
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.TEST_FILE_PATH,
+            "input_keys": cfg.MODEL.afno.input_keys,
+            "label_keys": output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "transforms": transforms,
+            "num_label_timestamps": cfg.EVAL.num_timestamps,
+            "training": False,
+            "stride": 8,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    # set metirc
+    metric = {
+        "MAE": ppsci.metric.MAE(keep_batch=True),
+        "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+            num_lat=cfg.IMG_H,
+            std=data_std,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+        "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+            num_lat=cfg.IMG_H,
+            mean=data_time_mean_normalize,
+            keep_batch=True,
+            variable_dict={"u10": 0, "v10": 1},
+        ),
+    }
+
+    # set validator for testing
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric=metric,
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set visualizer data
+    DATE_STRINGS = ("2018-09-08 00:00:00",)
+    vis_data = get_vis_data(
+        cfg.TEST_FILE_PATH,
+        DATE_STRINGS,
+        cfg.EVAL.num_timestamps,
+        cfg.VARS_CHANNEL,
+        cfg.IMG_H,
+        data_mean,
+        data_std,
+    )
+
+    def output_wind_func(d, var_name, data_mean, data_std):
+        output = (d[var_name] * data_std) + data_mean
+        wind_data = []
+        for i in range(output.shape[0]):
+            wind_data.append((output[i][0] ** 2 + output[i][1] ** 2) ** 0.5)
+        return paddle.to_tensor(wind_data, paddle.get_default_dtype())
+
+    vis_output_expr = {}
+    for i in range(cfg.EVAL.num_timestamps):
+        hour = (i + 1) * 6
+        vis_output_expr[f"output_{hour}h"] = functools.partial(
+            output_wind_func,
+            var_name=f"output_{i}",
+            data_mean=paddle.to_tensor(data_mean, paddle.get_default_dtype()),
+            data_std=paddle.to_tensor(data_std, paddle.get_default_dtype()),
+        )
+        vis_output_expr[f"target_{hour}h"] = lambda d, hour=hour: d[f"target_{hour}h"]
+    # set visualizer
+    visualizer = {
+        "visualize_wind": ppsci.visualize.VisualizerWeather(
+            vis_data,
+            vis_output_expr,
+            xticks=np.linspace(0, 1439, 13),
+            xticklabels=[str(i) for i in range(360, -1, -30)],
+            yticks=np.linspace(0, 719, 7),
+            yticklabels=[str(i) for i in range(90, -91, -30)],
+            vmin=0,
+            vmax=25,
+            colorbar_label="m\s",
+            batch_size=cfg.EVAL.batch_size,
+            num_timestamps=cfg.EVAL.num_timestamps,
+            prefix="wind",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    solver.eval()
+    # visualize prediction from pretrained_model_path
+    solver.visualize()
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="fourcastnet_finetune.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
examples/fourcastnet/train_precip.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import functools
+import os.path as osp
+from typing import Tuple
+
+import h5py
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import examples.fourcastnet.utils as fourcast_utils
+import ppsci
+from ppsci.utils import logger
+
+
+def get_vis_data(
+    wind_file_path: str,
+    file_path: str,
+    date_strings: Tuple[str, ...],
+    num_timestamps: int,
+    vars_channel: Tuple[int, ...],
+    img_h: int,
+    data_mean: np.ndarray,
+    data_std: np.ndarray,
+):
+    __wind_file = h5py.File(wind_file_path, "r")["fields"]
+    _file = h5py.File(file_path, "r")["tp"]
+    wind_data = []
+    data = []
+    for date_str in date_strings:
+        hours_since_jan_01_epoch = fourcast_utils.date_to_hours(date_str)
+        ic = int(hours_since_jan_01_epoch / 6)
+        wind_data.append(__wind_file[ic, vars_channel, 0:img_h])
+        data.append(_file[ic + 1 : ic + num_timestamps + 1, 0:img_h])
+    wind_data = np.asarray(wind_data)
+    data = np.asarray(data)
+
+    vis_data = {"input": (wind_data - data_mean) / data_std}
+    for t in range(num_timestamps):
+        hour = (t + 1) * 6
+        data_t = data[:, t]
+        vis_data[f"target_{hour}h"] = np.asarray(data_t)
+    return vis_data
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", f"{cfg.output_dir}/train.log", "info")
+
+    wind_data_mean, wind_data_std = fourcast_utils.get_mean_std(
+        cfg.WIND_MEAN_PATH, cfg.WIND_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W
+    )
+
+    # set train transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {
+            "Normalize": {
+                "mean": wind_data_mean,
+                "std": wind_data_std,
+                "apply_keys": ("input",),
+            }
+        },
+        {"Log1p": {"scale": 1e-5, "apply_keys": ("label",)}},
+    ]
+
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.WIND_TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.precip.input_keys,
+            "label_keys": cfg.MODEL.precip.output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "precip_file_path": cfg.TRAIN_FILE_PATH,
+            "transforms": transforms,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 8,
+    }
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+
+    # set eval dataloader config
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.WIND_VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.precip.input_keys,
+            "label_keys": cfg.MODEL.precip.output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "precip_file_path": cfg.VALID_FILE_PATH,
+            "transforms": transforms,
+            "training": False,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+
+    # set metric
+    metric = {
+        "MAE": ppsci.metric.MAE(keep_batch=True),
+        "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+            num_lat=cfg.IMG_H, keep_batch=True, unlog=True
+        ),
+        "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+            num_lat=cfg.IMG_H, mean=data_time_mean, keep_batch=True, unlog=True
+        ),
+    }
+
+    # set validator
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric=metric,
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set model
+    wind_model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+    ppsci.utils.save_load.load_pretrain(wind_model, path=cfg.WIND_MODEL_PATH)
+    model_cfg = dict(cfg.MODEL.precip)
+    model_cfg.update({"wind_model": wind_model})
+    model = ppsci.arch.PrecipNet(**model_cfg)
+
+    # init optimizer and lr scheduler
+    lr_scheduler_cfg = dict(cfg.TRAIN.lr_scheduler)
+    lr_scheduler_cfg.update({"iters_per_epoch": ITERS_PER_EPOCH})
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(**lr_scheduler_cfg)()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=True,
+        validator=validator,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set testing hyper-parameters
+    output_keys = tuple(f"output_{i}" for i in range(cfg.EVAL.num_timestamps))
+
+    # set model for testing
+    wind_model = ppsci.arch.AFNONet(**cfg.MODEL.afno)
+    ppsci.utils.save_load.load_pretrain(wind_model, path=cfg.WIND_MODEL_PATH)
+    model_cfg = dict(cfg.MODEL.precip)
+    model_cfg.update(
+        {
+            "output_keys": output_keys,
+            "num_timestamps": cfg.EVAL.num_timestamps,
+            "wind_model": wind_model,
+        }
+    )
+    model = ppsci.arch.PrecipNet(**model_cfg)
+
+    wind_data_mean, wind_data_std = fourcast_utils.get_mean_std(
+        cfg.WIND_MEAN_PATH, cfg.WIND_STD_PATH, cfg.VARS_CHANNEL
+    )
+    data_time_mean = fourcast_utils.get_time_mean(
+        cfg.TIME_MEAN_PATH, cfg.IMG_H, cfg.IMG_W
+    )
+
+    # set train transforms
+    transforms = [
+        {"SqueezeData": {}},
+        {"CropData": {"xmin": (0, 0), "xmax": (cfg.IMG_H, cfg.IMG_W)}},
+        {
+            "Normalize": {
+                "mean": wind_data_mean,
+                "std": wind_data_std,
+                "apply_keys": ("input",),
+            }
+        },
+        {"Log1p": {"scale": 1e-5, "apply_keys": ("label",)}},
+    ]
+
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "ERA5Dataset",
+            "file_path": cfg.WIND_TEST_FILE_PATH,
+            "input_keys": cfg.MODEL.precip.input_keys,
+            "label_keys": output_keys,
+            "vars_channel": cfg.VARS_CHANNEL,
+            "precip_file_path": cfg.TEST_FILE_PATH,
+            "num_label_timestamps": cfg.EVAL.num_timestamps,
+            "stride": 8,
+            "transforms": transforms,
+            "training": False,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    }
+    # set metirc
+    metric = {
+        "MAE": ppsci.metric.MAE(keep_batch=True),
+        "LatitudeWeightedRMSE": ppsci.metric.LatitudeWeightedRMSE(
+            num_lat=cfg.IMG_H, keep_batch=True, unlog=True
+        ),
+        "LatitudeWeightedACC": ppsci.metric.LatitudeWeightedACC(
+            num_lat=cfg.IMG_H, mean=data_time_mean, keep_batch=True, unlog=True
+        ),
+    }
+
+    # set validator for testing
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric=metric,
+        name="Sup_Validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set set visualizer data
+    DATE_STRINGS = ("2018-04-04 00:00:00",)
+    vis_data = get_vis_data(
+        cfg.WIND_TEST_FILE_PATH,
+        cfg.TEST_FILE_PATH,
+        DATE_STRINGS,
+        cfg.EVAL.num_timestamps,
+        cfg.VARS_CHANNEL,
+        cfg.IMG_H,
+        wind_data_mean,
+        wind_data_std,
+    )
+
+    def output_precip_func(d, var_name):
+        output = 1e-2 * paddle.expm1(d[var_name][0])
+        return output
+
+    visu_output_expr = {}
+    for i in range(cfg.EVAL.num_timestamps):
+        hour = (i + 1) * 6
+        visu_output_expr[f"output_{hour}h"] = functools.partial(
+            output_precip_func,
+            var_name=f"output_{i}",
+        )
+        visu_output_expr[f"target_{hour}h"] = (
+            lambda d, hour=hour: d[f"target_{hour}h"] * 1000
+        )
+    # set visualizer
+    visualizer = {
+        "visualize_precip": ppsci.visualize.VisualizerWeather(
+            vis_data,
+            visu_output_expr,
+            xticks=np.linspace(0, 1439, 13),
+            xticklabels=[str(i) for i in range(360, -1, -30)],
+            yticks=np.linspace(0, 719, 7),
+            yticklabels=[str(i) for i in range(90, -91, -30)],
+            vmin=0.001,
+            vmax=130,
+            colorbar_label="mm",
+            log_norm=True,
+            batch_size=cfg.EVAL.batch_size,
+            num_timestamps=cfg.EVAL.num_timestamps,
+            prefix="precip",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        compute_metric_by_batch=cfg.EVAL.compute_metric_by_batch,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    solver.eval()
+    # visualize prediction
+    solver.visualize()
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="fourcastnet_precip.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

6. 结果展示

+

下图展示了风速模型按照6小时间隔的预测结果和真值结果。

+
+

result_wind +

+
风速模型预测结果("output")与真值结果("target")
+
+

下图展示了降水量模型按照6小时间隔的预测结果和真值结果。

+
+

result_precip +

+
降水量模型预测结果("output")与真值结果("target")
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/heat_exchanger/index.html b/zh/examples/heat_exchanger/index.html new file mode 100644 index 0000000000..1fea18e57c --- /dev/null +++ b/zh/examples/heat_exchanger/index.html @@ -0,0 +1,5991 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heat_Exchanger - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Heat_Exchanger

+
+
+
+
python heat_exchanger.py
+
+
+
+
python heat_exchanger.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/HEDeepONet/HEDeepONet_pretrained.pdparams
+
+
+
+
python heat_exchanger.py mode=export
+
+
+
+
python heat_exchanger.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
heat_exchanger_pretrained.pdparamsThe L2 norm error between the actual heat exchanger efficiency and the predicted heat exchanger efficiency: 0.02087
MSE.heat_boundary(interior_mse): 0.52005
MSE.cold_boundary(interior_mse): 0.16590
MSE.wall(interior_mse): 0.01203
+

1. 背景简介

+

1.1 换热器

+

换热器(亦称为热交换器或热交换设备)是用来使热量从热流体传递到冷流体,以满足规定的工艺要求的装置,是对流传热及热传导的一种工业应用。

+

在一般空调设备中都有换热器,即空调室内机和室外机的冷热排;换热器作放热用时称为“冷凝器”,作吸热用时称为“蒸发器”,冷媒在此二者的物理反应相反。所以家用空调机作为冷气机时,室内机的换热器称作蒸发器,室外机的则称为冷凝器;换做暖气机的角色时,则相反称之,如图所示为蒸发循环制冷系统。研究换热器热仿真可以为优化设计、提高性能和可靠性、节能减排以及新技术研发提供重要的参考和指导。

+
+

heat_exchanger.png +

+
蒸发循环制冷系统
+
+

换热器在工程和科学领域具有多方面的重要性,其作用和价值主要体现在以下几个方面:

+
    +
  • 能源转换效率:换热器在能源转换中扮演着重要角色。通过优化热能的传递和利用,能够提高发电厂、工业生产和其他能源转换过程的效率。它们有助于将燃料中的热能转化为电能或机械能,最大限度地利用能源资源。
  • +
  • 工业生产优化:在化工、石油、制药等行业中,换热器用于加热、冷却、蒸馏和蒸发等工艺。通过有效的换热器设计和运用,可以改善生产效率、控制温度和压力,提高产品质量,并且减少能源消耗。
  • +
  • 温度控制与调节:换热器可以用于控制温度。在工业生产中,保持适当的温度对于反应速率、产品质量和设备寿命至关重要。换热器能够帮助调节和维持系统的温度在理想的操作范围内。
  • +
  • 环境保护与可持续发展:通过提高能源转换效率和工业生产过程中的能源利用率,换热器有助于减少对自然资源的依赖,并降低对环境的负面影响。能源效率的提高也可以减少温室气体排放,有利于环境保护和可持续发展。
  • +
  • 工程设计与创新:在工程设计领域,换热器的优化设计和创新推动了工程技术的发展。不断改进的换热器设计能够提高性能、减少空间占用并适应多种复杂工艺需求。
  • +
+

综上所述,换热器在工程和科学领域中的重要性体现在其对能源利用效率、工业生产过程优化、温度控制、环境保护和工程技术创新等方面的重要贡献。这些方面的不断改进和创新推动着工程技术的发展,有助于解决能源和环境方面的重要挑战。

+

2. 问题定义

+

2.1 问题描述

+

假设换热器内部流体流动是一维的,如图所示。

+
+

1DHE.png

+
+

忽略壁面的传热热阻和轴向热传导;与外界无热量交换,如图所示。则冷热流体和传热壁面三个节点的能量守恒方程分别为:

+
\[ +\begin{aligned} +& L\left(\frac{q_m c_p}{v}\right)_{\mathrm{c}} \frac{\partial T_{\mathrm{c}}}{\partial \tau}-L\left(q_m c_p\right)_{\mathrm{c}} \frac{\partial T_{\mathrm{c}}}{\partial x}=\left(\eta_{\mathrm{o}} \alpha A\right)_{\mathrm{c}}\left(T_{\mathrm{w}}-T_{\mathrm{c}}\right), \\ +& L\left(\frac{q_m c_p}{v}\right)_{\mathrm{h}} \frac{\partial T_{\mathrm{h}}}{\partial \tau}+L\left(q_m c_p\right)_{\mathrm{h}} \frac{\partial T_{\mathrm{h}}}{\partial x}=\left(\eta_{\mathrm{o}} \alpha A\right)_{\mathrm{h}}\left(T_{\mathrm{w}}-T_{\mathrm{h}}\right), \\ +& \left(M c_p\right)_{\mathrm{w}} \frac{\partial T_{\mathrm{w}}}{\partial \tau}=\left(\eta_{\mathrm{o}} \alpha A\right)_{\mathrm{h}}\left(T_{\mathrm{h}}-T_{\mathrm{w}}\right)+\left(\eta_{\mathrm{o}} \alpha A\right)_{\mathrm{c}}\left(T_{\mathrm{c}}-T_{\mathrm{w}}\right). +\end{aligned} +\]
+

其中:

+
    +
  • \(T\) 代表温度,
  • +
  • \(q_m\) 代表质量流量,
  • +
  • \(c_p\) 代表比热容,
  • +
  • \(v\) 代表流速,
  • +
  • \(L\) 代表流动长度,
  • +
  • \(\eta_{\mathrm{o}}\) 代表翅片表面效率,
  • +
  • \(\alpha\) 代表传热系数,
  • +
  • \(A\) 代表传热面积,
  • +
  • \(M\) 代表传热结构的质量,
  • +
  • \(\tau\) 代表对应时间,
  • +
  • \(x\) 代表流动方向,
  • +
  • 下标 \(\mathrm{h}\)\(\mathrm{c}\)\(\mathrm{w}\) 分别表示热边流体、冷边流体和换热壁面。
  • +
+

换热器冷、热流体进出口参数满足能量守恒, 即:

+
\[ +\left(q_m c_p\right)_{\mathrm{h}}\left(T_{\mathrm{h}, \text { in }}-T_{\mathrm{h}, \text { out }}\right)=\left(q_m c_p\right)_c\left(T_{\mathrm{c}, \text {out }}-T_{\mathrm{c}, \text {in }}\right). +\]
+

换热器效率 \(\eta\) 为实际传热量与理论最大的传热量之比,即:

+
\[ +\eta=\frac{\left(q_m c_p\right)_{\mathrm{h}}\left(T_{\mathrm{h}, \text { in }}-T_{\mathrm{h}, \text { out }}\right)}{\left(q_m c_p\right)_{\text {min }}\left(T_{\mathrm{h}, \text { in }}-T_{\mathrm{c}, \text { in }}\right)}, +\]
+

式中,下标 \(min\) 表示冷热流体热容较小值。

+

2.2 PI-DeepONet模型

+

PI-DeepONet模型,将 DeepONet 和 PINN 方法相结合,是一种结合了物理信息和算子学习的深度神经网络模型。这种模型可以通过控制方程的物理信息来增强 DeepONet 模型,同时可以将不同的 PDE 配置分别作为不同的分支网络的输入数据,从而可以有效地用于在各种(参数和非参数)PDE 配置下进行超快速的模型预测。

+

对于换热器问题,PI-DeepONet 模型可以表示为如图所示的模型结构:

+
+

PI-DeepONet.png

+
+

如图所示,我们一共使用了 2 个分支网络和一个主干网络,分支网络分别输入热边的质量流量和冷边的质量流量,主干网络输入一维坐标点坐标和时间信息。每个分支网和主干网均输出 \(q\) 维特征向量,通过Hadamard(逐元素)乘积组合所有这些输出特征,然后将所得向量相加为预测温度场的标量输出。

+

3. 问题求解

+

接下来开始讲解如何将该问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该换热器热仿真问题。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建等关键步骤进行阐述,而其余细节请参考API文档

+

3.1 模型构建

+

在换热器热仿真问题中,每一个已知的坐标点 \((t, x)\) 和每一组热边的质量流量和冷边的质量流量 \((q_{mh}, q_{mc})\) 都对应一组热边流体的温度 \(T_h\) 、冷边流体的温度 \(T_c\) 和换热壁面的温度 \(T_h\) 三个待求解的未知量。我们在这里使用 2 个分支网络和一个主干网络,3 个网络均为 MLP(Multilayer Perceptron, 多层感知机) 。 2 个分支网络分别表示 \((q_{mh}, q_{mc})\) 到输出函数 \((b_1,b_2)\) 的映射函数 \(f_1,f_2: \mathbb{R}^2 \to \mathbb{R}^{3q}\),即:

+
\[ +\begin{aligned} +b_1 &= f_1(q_{mh}),\\ +b_2 &= f_2(q_{mc}). +\end{aligned} +\]
+

上式中 \(f_1,f_2\) 均为 MLP 模型,\((b_1,b_2)\) 分别为两个分支网络的输出函数,\(3q\) 为输出函数的维数。主干网络表示 \((t, x)\) 到输出函数 \(t_0\) 的映射函数 \(f_3: \mathbb{R}^2 \to \mathbb{R}^{3q}\),即:

+
\[ +\begin{aligned} +t_0 &= f_3(t,x). +\end{aligned} +\]
+

上式中 \(f_3\) 为 MLP 模型,\((t_0)\) 为主支网络的输出函数,\(3q\) 为输出函数的维数。我们可以将两个分支网络和主干网络的输出函数 \((b_1,b_2, t_0)\) 分成3组,然后对每一组的输出函数分别进行Hadamard(逐元素)乘积再相加得到标量温度场,即:

+
\[ +\begin{aligned} +T_h &= \sum_{i=1}^q b_1^ib_2^i t_0^i,\\ +T_c &= \sum_{i=q+1}^{2q} b_1^ib_2^i t_0^i,\\ +T_w &= \sum_{i=2q+1}^{3q} b_1^ib_2^i t_0^i. +\end{aligned} +\]
+

我们定义 PaddleScience 内置的 HEDeepONets 模型类,并调用,PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.HEDeepONets(**cfg.MODEL)
+
+

这样我们就实例化出了一个拥有 3 个 MLP 模型的 HEDeepONets 模型,每个分支网络包含 9 层隐藏神经元,每层神经元数为 256,主干网络包含 6 层隐藏神经元,每层神经元数为 128,使用 "swish" 作为激活函数,并包含三个输出函数 \(T_h,T_c,T_w\) 的神经网络模型 model

+

3.2 计算域构建

+

对本文中换热器问题构造训练区域,即以 [0, 1] 的一维区域,且时间域为 21 个时刻 [0,1,2,...,21],该区域可以直接使用 PaddleScience 内置的空间几何 Interval 和时间域 TimeDomain,组合成时间-空间的 TimeXGeometry 计算域。代码如下

+
36
+37
+38
+39
+40
+41
+42
+43
# set time-geometry
+timestamps = np.linspace(0.0, 2, cfg.NTIME + 1, endpoint=True)
+geom = {
+    "time_rect": ppsci.geometry.TimeXGeometry(
+        ppsci.geometry.TimeDomain(0.0, 1, timestamps=timestamps),
+        ppsci.geometry.Interval(0, cfg.DL),
+    )
+}
+
+
+提示 +

RectangleTimeDomain 是两种可以单独使用的 Geometry 派生类。

+

如输入数据只来自于二维矩形几何域,则可以直接使用 ppsci.geometry.Rectangle(...) 创建空间几何域对象;

+

如输入数据只来自一维时间域,则可以直接使用 ppsci.geometry.TimeDomain(...) 构建时间域对象。

+
+

3.3 输入数据构建

+
    +
  • 通过 TimeXGeometry 计算域来构建输入的时间和空间均匀数据,
  • +
  • 通过 np.random.rand 来生成 (0,2) 之间的随机数,这些随机数用于构建热边和冷边的质量流量的训练和测试数据。
  • +
+

对时间、空间均匀数据和热边、冷边的质量流量数据进行组合,得到最终的训练和测试输入数据。代码如下

+
45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
# Generate train data and eval data
+visu_input = geom["time_rect"].sample_interior(cfg.NPOINT * cfg.NTIME, evenly=True)
+data_h = np.random.rand(cfg.NQM).reshape([-1, 1]) * 2
+data_c = np.random.rand(cfg.NQM).reshape([-1, 1]) * 2
+data_h = data_h.astype("float32")
+data_c = data_c.astype("float32")
+test_h = np.random.rand(1).reshape([-1, 1]).astype("float32")
+test_c = np.random.rand(1).reshape([-1, 1]).astype("float32")
+# rearrange train data and eval data
+points = visu_input.copy()
+points["t"] = np.repeat(points["t"], cfg.NQM, axis=0)
+points["x"] = np.repeat(points["x"], cfg.NQM, axis=0)
+points["qm_h"] = np.tile(data_h, (cfg.NPOINT * cfg.NTIME, 1))
+points["t"] = np.repeat(points["t"], cfg.NQM, axis=0)
+points["x"] = np.repeat(points["x"], cfg.NQM, axis=0)
+points["qm_h"] = np.repeat(points["qm_h"], cfg.NQM, axis=0)
+points["qm_c"] = np.tile(data_c, (cfg.NPOINT * cfg.NTIME * cfg.NQM, 1))
+visu_input["qm_h"] = np.tile(test_h, (cfg.NPOINT * cfg.NTIME, 1))
+visu_input["qm_c"] = np.tile(test_c, (cfg.NPOINT * cfg.NTIME, 1))
+
+

然后对训练数据按照空间坐标和时间进行分类,将训练数据和测试数据分类成左边界数据、内部数据、右边界数据以及初值数据。代码如下

+
left_indices = visu_input["x"] == 0
+right_indices = visu_input["x"] == cfg.DL
+interior_indices = (visu_input["x"] != 0) & (visu_input["x"] != cfg.DL)
+left_indices = np.where(left_indices)
+right_indices = np.where(right_indices)
+interior_indices = np.where(interior_indices)
+
+left_indices1 = points["x"] == 0
+right_indices1 = points["x"] == cfg.DL
+interior_indices1 = (points["x"] != 0) & (points["x"] != cfg.DL)
+initial_indices1 = points["t"] == points["t"][0]
+left_indices1 = np.where(left_indices1)
+right_indices1 = np.where(right_indices1)
+interior_indices1 = np.where(interior_indices1)
+initial_indices1 = np.where(initial_indices1)
+
+# Classification train data
+left_data = {
+    "x": points["x"][left_indices1[0]],
+    "t": points["t"][left_indices1[0]],
+    "qm_h": points["qm_h"][left_indices1[0]],
+    "qm_c": points["qm_c"][left_indices1[0]],
+}
+right_data = {
+    "x": points["x"][right_indices1[0]],
+    "t": points["t"][right_indices1[0]],
+    "qm_h": points["qm_h"][right_indices1[0]],
+    "qm_c": points["qm_c"][right_indices1[0]],
+}
+interior_data = {
+    "x": points["x"],
+    "t": points["t"],
+    "qm_h": points["qm_h"],
+    "qm_c": points["qm_c"],
+}
+initial_data = {
+    "x": points["x"][initial_indices1[0]],
+    "t": points["t"][initial_indices1[0]] * 0,
+    "qm_h": points["qm_h"][initial_indices1[0]],
+    "qm_c": points["qm_c"][initial_indices1[0]],
+}
+# Classification eval data
+test_left_data = {
+    "x": visu_input["x"][left_indices[0]],
+    "t": visu_input["t"][left_indices[0]],
+    "qm_h": visu_input["qm_h"][left_indices[0]],
+    "qm_c": visu_input["qm_c"][left_indices[0]],
+}
+test_right_data = {
+    "x": visu_input["x"][right_indices[0]],
+    "t": visu_input["t"][right_indices[0]],
+    "qm_h": visu_input["qm_h"][right_indices[0]],
+    "qm_c": visu_input["qm_c"][right_indices[0]],
+}
+test_interior_data = {
+    "x": visu_input["x"],
+    "t": visu_input["t"],
+    "qm_h": visu_input["qm_h"],
+    "qm_c": visu_input["qm_c"],
+}
+
+

3.4 方程构建

+

换热器热仿真问题由 2.1 问题描述 中描述的方程组成,这里我们定义 PaddleScience 内置的 HeatEquation 方程类来构建该方程。指定该类的参数均为1,代码如下

+
# set equation
+equation = {
+    "heat_exchanger": ppsci.equation.HeatExchanger(
+        cfg.alpha_h / (cfg.L * cfg.cp_h),
+        cfg.alpha_c / (cfg.L * cfg.cp_c),
+        cfg.v_h,
+        cfg.v_c,
+        cfg.alpha_h / (cfg.M * cfg.cp_w),
+        cfg.alpha_c / (cfg.M * cfg.cp_w),
+    )
+}
+
+

3.5 约束构建

+

换热器热仿真问题由 2.1 问题描述 中描述的方程组成,我们设置以下边界条件:

+
\[ +\begin{aligned} +T_h(t,0) &= 10,\\ +T_c(t,1) &= 1. +\end{aligned} +\]
+

同时,我们设置初值条件:

+
\[ +\begin{aligned} +T_h(0,x) &= 10,\\ +T_c(0,x) &= 1,\\ +T_w(0,x) &= 5.5. +\end{aligned} +\]
+

此时我们对左边界数据、内部数据、右边界数据以及初值数据设置四个约束条件,接下来使用 PaddleScience 内置的 SupervisedConstraint 构建上述四种约束条件,代码如下

+
# set constraint
+bc_label = {
+    "T_h": np.zeros([left_data["x"].shape[0], 1], dtype="float32"),
+}
+interior_label = {
+    "heat_boundary": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+    "cold_boundary": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+    "wall": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+}
+initial_label = {
+    "T_h": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+    "T_c": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+    "T_w": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+}
+
+left_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": left_data,
+            "label": bc_label,
+            "weight": {
+                "T_h": np.full_like(
+                    left_data["x"], cfg.TRAIN.weight.left_sup_constraint.T_h
+                )
+            },
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"T_h": lambda out: out["T_h"] - cfg.T_hin},
+    name="left_sup",
+)
+right_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": right_data,
+            "label": bc_label,
+            "weight": {
+                "T_h": np.full_like(
+                    right_data["x"], cfg.TRAIN.weight.right_sup_constraint.T_h
+                )
+            },
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"T_h": lambda out: out["T_c"] - cfg.T_cin},
+    name="right_sup",
+)
+interior_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": interior_data,
+            "label": interior_label,
+            "weight": {
+                "heat_boundary": np.full_like(
+                    interior_data["x"],
+                    cfg.TRAIN.weight.interior_sup_constraint.heat_boundary,
+                ),
+                "cold_boundary": np.full_like(
+                    interior_data["x"],
+                    cfg.TRAIN.weight.interior_sup_constraint.cold_boundary,
+                ),
+                "wall": np.full_like(
+                    interior_data["x"],
+                    cfg.TRAIN.weight.interior_sup_constraint.wall,
+                ),
+            },
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr=equation["heat_exchanger"].equations,
+    name="interior_sup",
+)
+initial_sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": initial_data,
+            "label": initial_label,
+            "weight": {
+                "T_h": np.full_like(
+                    initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_h
+                ),
+                "T_c": np.full_like(
+                    initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_c
+                ),
+                "T_w": np.full_like(
+                    initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_w
+                ),
+            },
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={
+        "T_h": lambda out: out["T_h"] - cfg.T_hin,
+        "T_c": lambda out: out["T_c"] - cfg.T_cin,
+        "T_w": lambda out: out["T_w"] - cfg.T_win,
+    },
+    name="initial_sup",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,其中 “dataset” 字段表示使用的训练数据集信息,各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 "NamedArrayDataset" 表示分 batch 顺序读取数据;
  2. +
  3. input: 输入变量名;
  4. +
  5. label: 标签变量名;
  6. +
  7. weight: 权重大小。
  8. +
+

"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,另外还指定了该类初始化时参数 drop_lastFalseshuffleTrue

+

第二个参数是损失函数,此处我们选用常用的 MSE 函数,且 reduction"mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第三个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在微分方程约束和监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    left_sup_constraint.name: left_sup_constraint,
+    right_sup_constraint.name: right_sup_constraint,
+    interior_sup_constraint.name: interior_sup_constraint,
+    initial_sup_constraint.name: initial_sup_constraint,
+}
+
+

3.6 优化器构建

+

接下来我们需要指定学习率,学习率设为 0.001,训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,我们使用 ppsci.validate.SupervisedValidator 构建评估器。

+
# set validator
+test_bc_label = {
+    "T_h": np.zeros([test_left_data["x"].shape[0], 1], dtype="float32"),
+}
+test_interior_label = {
+    "heat_boundary": np.zeros(
+        [test_interior_data["x"].shape[0], 1], dtype="float32"
+    ),
+    "cold_boundary": np.zeros(
+        [test_interior_data["x"].shape[0], 1], dtype="float32"
+    ),
+    "wall": np.zeros([test_interior_data["x"].shape[0], 1], dtype="float32"),
+}
+left_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_left_data,
+            "label": test_bc_label,
+        },
+        "batch_size": cfg.NTIME,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"T_h": lambda out: out["T_h"] - cfg.T_hin},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="left_mse",
+)
+right_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_right_data,
+            "label": test_bc_label,
+        },
+        "batch_size": cfg.NTIME,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr={"T_h": lambda out: out["T_c"] - cfg.T_cin},
+    metric={"MSE": ppsci.metric.MSE()},
+    name="right_mse",
+)
+interior_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": test_interior_data,
+            "label": test_interior_label,
+        },
+        "batch_size": cfg.NTIME,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr=equation["heat_exchanger"].equations,
+    metric={"MSE": ppsci.metric.MSE()},
+    name="interior_mse",
+)
+validator = {
+    left_validator.name: left_validator,
+    right_validator.name: right_validator,
+    interior_validator.name: interior_validator,
+}
+
+

配置与 3.5 约束构建 的设置类似。

+

3.8 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# plotting iteration/epoch-loss curve.
+solver.plot_loss_history()
+
+

3.9 结果可视化

+

最后在给定的可视化区域上进行预测并可视化,设冷边和热边的质量流量均为1,可视化数据是区域内的一维点集,每个时刻 \(t\) 对应的坐标是 \(x^i\),对应值是 \((T_h^{i}, T_c^i, T_w^i)\),在此我们画出 \(T_h,T_c,T_w\) 随时间的变化图像。同时根据换热器效率的公式计算出换热器效率 \(\eta\) ,画出换热器效率 \(\eta\) 随时间的变化图像,代码如下:

+
    # visualize prediction after finished training
+    visu_input["qm_c"] = np.full_like(visu_input["qm_c"], cfg.qm_h)
+    visu_input["qm_h"] = np.full_like(visu_input["qm_c"], cfg.qm_c)
+    pred = solver.predict(visu_input, return_numpy=True)
+    plot(visu_input, pred, cfg)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.HEDeepONets(**cfg.MODEL)
+
+    # set time-geometry
+    timestamps = np.linspace(0.0, 2, cfg.NTIME + 1, endpoint=True)
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1, timestamps=timestamps),
+            ppsci.geometry.Interval(0, cfg.DL),
+        )
+    }
+
+    # Generate eval data
+    visu_input = geom["time_rect"].sample_interior(cfg.NPOINT * cfg.NTIME, evenly=True)
+    test_h = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    test_c = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    # rearrange train data and eval data
+    visu_input["qm_h"] = np.tile(test_h, (cfg.NPOINT * cfg.NTIME, 1))
+    visu_input["qm_c"] = np.tile(test_c, (cfg.NPOINT * cfg.NTIME, 1))
+
+    left_indices = visu_input["x"] == 0
+    right_indices = visu_input["x"] == cfg.DL
+    interior_indices = (visu_input["x"] != 0) & (visu_input["x"] != cfg.DL)
+    left_indices = np.where(left_indices)
+    right_indices = np.where(right_indices)
+    interior_indices = np.where(interior_indices)
+
+    # Classification eval data
+    test_left_data = {
+        "x": visu_input["x"][left_indices[0]],
+        "t": visu_input["t"][left_indices[0]],
+        "qm_h": visu_input["qm_h"][left_indices[0]],
+        "qm_c": visu_input["qm_c"][left_indices[0]],
+    }
+    test_right_data = {
+        "x": visu_input["x"][right_indices[0]],
+        "t": visu_input["t"][right_indices[0]],
+        "qm_h": visu_input["qm_h"][right_indices[0]],
+        "qm_c": visu_input["qm_c"][right_indices[0]],
+    }
+    test_interior_data = {
+        "x": visu_input["x"],
+        "t": visu_input["t"],
+        "qm_h": visu_input["qm_h"],
+        "qm_c": visu_input["qm_c"],
+
+

4. 完整代码

+
heat_exchanger.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from os import path as osp
+
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.HEDeepONets(**cfg.MODEL)
+
+    # set time-geometry
+    timestamps = np.linspace(0.0, 2, cfg.NTIME + 1, endpoint=True)
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1, timestamps=timestamps),
+            ppsci.geometry.Interval(0, cfg.DL),
+        )
+    }
+
+    # Generate train data and eval data
+    visu_input = geom["time_rect"].sample_interior(cfg.NPOINT * cfg.NTIME, evenly=True)
+    data_h = np.random.rand(cfg.NQM).reshape([-1, 1]) * 2
+    data_c = np.random.rand(cfg.NQM).reshape([-1, 1]) * 2
+    data_h = data_h.astype("float32")
+    data_c = data_c.astype("float32")
+    test_h = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    test_c = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    # rearrange train data and eval data
+    points = visu_input.copy()
+    points["t"] = np.repeat(points["t"], cfg.NQM, axis=0)
+    points["x"] = np.repeat(points["x"], cfg.NQM, axis=0)
+    points["qm_h"] = np.tile(data_h, (cfg.NPOINT * cfg.NTIME, 1))
+    points["t"] = np.repeat(points["t"], cfg.NQM, axis=0)
+    points["x"] = np.repeat(points["x"], cfg.NQM, axis=0)
+    points["qm_h"] = np.repeat(points["qm_h"], cfg.NQM, axis=0)
+    points["qm_c"] = np.tile(data_c, (cfg.NPOINT * cfg.NTIME * cfg.NQM, 1))
+    visu_input["qm_h"] = np.tile(test_h, (cfg.NPOINT * cfg.NTIME, 1))
+    visu_input["qm_c"] = np.tile(test_c, (cfg.NPOINT * cfg.NTIME, 1))
+
+    left_indices = visu_input["x"] == 0
+    right_indices = visu_input["x"] == cfg.DL
+    interior_indices = (visu_input["x"] != 0) & (visu_input["x"] != cfg.DL)
+    left_indices = np.where(left_indices)
+    right_indices = np.where(right_indices)
+    interior_indices = np.where(interior_indices)
+
+    left_indices1 = points["x"] == 0
+    right_indices1 = points["x"] == cfg.DL
+    interior_indices1 = (points["x"] != 0) & (points["x"] != cfg.DL)
+    initial_indices1 = points["t"] == points["t"][0]
+    left_indices1 = np.where(left_indices1)
+    right_indices1 = np.where(right_indices1)
+    interior_indices1 = np.where(interior_indices1)
+    initial_indices1 = np.where(initial_indices1)
+
+    # Classification train data
+    left_data = {
+        "x": points["x"][left_indices1[0]],
+        "t": points["t"][left_indices1[0]],
+        "qm_h": points["qm_h"][left_indices1[0]],
+        "qm_c": points["qm_c"][left_indices1[0]],
+    }
+    right_data = {
+        "x": points["x"][right_indices1[0]],
+        "t": points["t"][right_indices1[0]],
+        "qm_h": points["qm_h"][right_indices1[0]],
+        "qm_c": points["qm_c"][right_indices1[0]],
+    }
+    interior_data = {
+        "x": points["x"],
+        "t": points["t"],
+        "qm_h": points["qm_h"],
+        "qm_c": points["qm_c"],
+    }
+    initial_data = {
+        "x": points["x"][initial_indices1[0]],
+        "t": points["t"][initial_indices1[0]] * 0,
+        "qm_h": points["qm_h"][initial_indices1[0]],
+        "qm_c": points["qm_c"][initial_indices1[0]],
+    }
+    # Classification eval data
+    test_left_data = {
+        "x": visu_input["x"][left_indices[0]],
+        "t": visu_input["t"][left_indices[0]],
+        "qm_h": visu_input["qm_h"][left_indices[0]],
+        "qm_c": visu_input["qm_c"][left_indices[0]],
+    }
+    test_right_data = {
+        "x": visu_input["x"][right_indices[0]],
+        "t": visu_input["t"][right_indices[0]],
+        "qm_h": visu_input["qm_h"][right_indices[0]],
+        "qm_c": visu_input["qm_c"][right_indices[0]],
+    }
+    test_interior_data = {
+        "x": visu_input["x"],
+        "t": visu_input["t"],
+        "qm_h": visu_input["qm_h"],
+        "qm_c": visu_input["qm_c"],
+    }
+
+    # set equation
+    equation = {
+        "heat_exchanger": ppsci.equation.HeatExchanger(
+            cfg.alpha_h / (cfg.L * cfg.cp_h),
+            cfg.alpha_c / (cfg.L * cfg.cp_c),
+            cfg.v_h,
+            cfg.v_c,
+            cfg.alpha_h / (cfg.M * cfg.cp_w),
+            cfg.alpha_c / (cfg.M * cfg.cp_w),
+        )
+    }
+
+    # set constraint
+    bc_label = {
+        "T_h": np.zeros([left_data["x"].shape[0], 1], dtype="float32"),
+    }
+    interior_label = {
+        "heat_boundary": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+        "cold_boundary": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+        "wall": np.zeros([interior_data["x"].shape[0], 1], dtype="float32"),
+    }
+    initial_label = {
+        "T_h": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+        "T_c": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+        "T_w": np.zeros([initial_data["x"].shape[0], 1], dtype="float32"),
+    }
+
+    left_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": left_data,
+                "label": bc_label,
+                "weight": {
+                    "T_h": np.full_like(
+                        left_data["x"], cfg.TRAIN.weight.left_sup_constraint.T_h
+                    )
+                },
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"T_h": lambda out: out["T_h"] - cfg.T_hin},
+        name="left_sup",
+    )
+    right_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": right_data,
+                "label": bc_label,
+                "weight": {
+                    "T_h": np.full_like(
+                        right_data["x"], cfg.TRAIN.weight.right_sup_constraint.T_h
+                    )
+                },
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"T_h": lambda out: out["T_c"] - cfg.T_cin},
+        name="right_sup",
+    )
+    interior_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": interior_data,
+                "label": interior_label,
+                "weight": {
+                    "heat_boundary": np.full_like(
+                        interior_data["x"],
+                        cfg.TRAIN.weight.interior_sup_constraint.heat_boundary,
+                    ),
+                    "cold_boundary": np.full_like(
+                        interior_data["x"],
+                        cfg.TRAIN.weight.interior_sup_constraint.cold_boundary,
+                    ),
+                    "wall": np.full_like(
+                        interior_data["x"],
+                        cfg.TRAIN.weight.interior_sup_constraint.wall,
+                    ),
+                },
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr=equation["heat_exchanger"].equations,
+        name="interior_sup",
+    )
+    initial_sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": initial_data,
+                "label": initial_label,
+                "weight": {
+                    "T_h": np.full_like(
+                        initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_h
+                    ),
+                    "T_c": np.full_like(
+                        initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_c
+                    ),
+                    "T_w": np.full_like(
+                        initial_data["x"], cfg.TRAIN.weight.initial_sup_constraint.T_w
+                    ),
+                },
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "T_h": lambda out: out["T_h"] - cfg.T_hin,
+            "T_c": lambda out: out["T_c"] - cfg.T_cin,
+            "T_w": lambda out: out["T_w"] - cfg.T_win,
+        },
+        name="initial_sup",
+    )
+    # wrap constraints together
+    constraint = {
+        left_sup_constraint.name: left_sup_constraint,
+        right_sup_constraint.name: right_sup_constraint,
+        interior_sup_constraint.name: interior_sup_constraint,
+        initial_sup_constraint.name: initial_sup_constraint,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    test_bc_label = {
+        "T_h": np.zeros([test_left_data["x"].shape[0], 1], dtype="float32"),
+    }
+    test_interior_label = {
+        "heat_boundary": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        ),
+        "cold_boundary": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        ),
+        "wall": np.zeros([test_interior_data["x"].shape[0], 1], dtype="float32"),
+    }
+    left_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_left_data,
+                "label": test_bc_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"T_h": lambda out: out["T_h"] - cfg.T_hin},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="left_mse",
+    )
+    right_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_right_data,
+                "label": test_bc_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={"T_h": lambda out: out["T_c"] - cfg.T_cin},
+        metric={"MSE": ppsci.metric.MSE()},
+        name="right_mse",
+    )
+    interior_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_interior_data,
+                "label": test_interior_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr=equation["heat_exchanger"].equations,
+        metric={"MSE": ppsci.metric.MSE()},
+        name="interior_mse",
+    )
+    validator = {
+        left_validator.name: left_validator,
+        right_validator.name: right_validator,
+        interior_validator.name: interior_validator,
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # plotting iteration/epoch-loss curve.
+    solver.plot_loss_history()
+
+    # visualize prediction after finished training
+    visu_input["qm_c"] = np.full_like(visu_input["qm_c"], cfg.qm_h)
+    visu_input["qm_h"] = np.full_like(visu_input["qm_c"], cfg.qm_c)
+    pred = solver.predict(visu_input, return_numpy=True)
+    plot(visu_input, pred, cfg)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set model
+    model = ppsci.arch.HEDeepONets(**cfg.MODEL)
+
+    # set time-geometry
+    timestamps = np.linspace(0.0, 2, cfg.NTIME + 1, endpoint=True)
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1, timestamps=timestamps),
+            ppsci.geometry.Interval(0, cfg.DL),
+        )
+    }
+
+    # Generate eval data
+    visu_input = geom["time_rect"].sample_interior(cfg.NPOINT * cfg.NTIME, evenly=True)
+    test_h = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    test_c = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    # rearrange train data and eval data
+    visu_input["qm_h"] = np.tile(test_h, (cfg.NPOINT * cfg.NTIME, 1))
+    visu_input["qm_c"] = np.tile(test_c, (cfg.NPOINT * cfg.NTIME, 1))
+
+    left_indices = visu_input["x"] == 0
+    right_indices = visu_input["x"] == cfg.DL
+    interior_indices = (visu_input["x"] != 0) & (visu_input["x"] != cfg.DL)
+    left_indices = np.where(left_indices)
+    right_indices = np.where(right_indices)
+    interior_indices = np.where(interior_indices)
+
+    # Classification eval data
+    test_left_data = {
+        "x": visu_input["x"][left_indices[0]],
+        "t": visu_input["t"][left_indices[0]],
+        "qm_h": visu_input["qm_h"][left_indices[0]],
+        "qm_c": visu_input["qm_c"][left_indices[0]],
+    }
+    test_right_data = {
+        "x": visu_input["x"][right_indices[0]],
+        "t": visu_input["t"][right_indices[0]],
+        "qm_h": visu_input["qm_h"][right_indices[0]],
+        "qm_c": visu_input["qm_c"][right_indices[0]],
+    }
+    test_interior_data = {
+        "x": visu_input["x"],
+        "t": visu_input["t"],
+        "qm_h": visu_input["qm_h"],
+        "qm_c": visu_input["qm_c"],
+    }
+
+    # set equation
+    equation = {
+        "heat_exchanger": ppsci.equation.HeatExchanger(
+            cfg.alpha_h / (cfg.L * cfg.cp_h),
+            cfg.alpha_c / (cfg.L * cfg.cp_c),
+            cfg.v_h,
+            cfg.v_c,
+            cfg.alpha_h / (cfg.M * cfg.cp_w),
+            cfg.alpha_c / (cfg.M * cfg.cp_w),
+        )
+    }
+
+    # set validator
+    test_bc_label = {
+        "T_h": np.zeros([test_left_data["x"].shape[0], 1], dtype="float32"),
+    }
+    test_interior_label = {
+        "heat_boundary": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        ),
+        "cold_boundary": np.zeros(
+            [test_interior_data["x"].shape[0], 1], dtype="float32"
+        ),
+        "wall": np.zeros([test_interior_data["x"].shape[0], 1], dtype="float32"),
+    }
+    left_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_left_data,
+                "label": test_bc_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "T_h": lambda out: out["T_h"] - cfg.T_hin,
+        },
+        metric={"MSE": ppsci.metric.MSE()},
+        name="left_mse",
+    )
+    right_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_right_data,
+                "label": test_bc_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr={
+            "T_h": lambda out: out["T_c"] - cfg.T_cin,
+        },
+        metric={"MSE": ppsci.metric.MSE()},
+        name="right_mse",
+    )
+    interior_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": test_interior_data,
+                "label": test_interior_label,
+            },
+            "batch_size": cfg.NTIME,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr=equation["heat_exchanger"].equations,
+        metric={"MSE": ppsci.metric.MSE()},
+        name="interior_mse",
+    )
+    validator = {
+        left_validator.name: left_validator,
+        right_validator.name: right_validator,
+        interior_validator.name: interior_validator,
+    }
+
+    # directly evaluate pretrained model(optional)
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+
+    # visualize prediction after finished training
+    visu_input["qm_c"] = np.full_like(visu_input["qm_c"], cfg.qm_h)
+    visu_input["qm_h"] = np.full_like(visu_input["qm_c"], cfg.qm_c)
+    pred = solver.predict(visu_input, return_numpy=True)
+    plot(visu_input, pred, cfg)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.HEDeepONets(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set time-geometry
+    timestamps = np.linspace(0.0, 2, cfg.NTIME + 1, endpoint=True)
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1, timestamps=timestamps),
+            ppsci.geometry.Interval(0, cfg.DL),
+        )
+    }
+    input_dict = geom["time_rect"].sample_interior(cfg.NPOINT * cfg.NTIME, evenly=True)
+    test_h = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    test_c = np.random.rand(1).reshape([-1, 1]).astype("float32")
+    # rearrange train data and eval data
+    input_dict["qm_h"] = np.tile(test_h, (cfg.NPOINT * cfg.NTIME, 1))
+    input_dict["qm_c"] = np.tile(test_c, (cfg.NPOINT * cfg.NTIME, 1))
+    input_dict["qm_c"] = np.full_like(input_dict["qm_c"], cfg.qm_h)
+    input_dict["qm_h"] = np.full_like(input_dict["qm_c"], cfg.qm_c)
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.INFER.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+    plot(input_dict, output_dict, cfg)
+
+
+def plot(visu_input, pred, cfg: DictConfig):
+    x = visu_input["x"][: cfg.NPOINT]
+    # plot temperature of heat boundary
+    plt.figure()
+    y = np.full_like(pred["T_h"][: cfg.NPOINT], cfg.T_hin)
+    plt.plot(x, y, label="t = 0.0 s")
+    for i in range(10):
+        y = pred["T_h"][cfg.NPOINT * i * 2 : cfg.NPOINT * (i * 2 + 1)]
+        plt.plot(x, y, label=f"t = {(i+1)*0.1:,.1f} s")
+    plt.xlabel("A")
+    plt.ylabel(r"$T_h$")
+    plt.legend()
+    plt.grid()
+    plt.savefig("T_h.png")
+    # plot temperature of cold boundary
+    plt.figure()
+    y = np.full_like(pred["T_c"][: cfg.NPOINT], cfg.T_cin)
+    plt.plot(x, y, label="t = 0.0 s")
+    for i in range(10):
+        y = pred["T_c"][cfg.NPOINT * i * 2 : cfg.NPOINT * (i * 2 + 1)]
+        plt.plot(x, y, label=f"t = {(i+1)*0.1:,.1f} s")
+    plt.xlabel("A")
+    plt.ylabel(r"$T_c$")
+    plt.legend()
+    plt.grid()
+    plt.savefig("T_c.png")
+    # plot temperature of wall
+    plt.figure()
+    y = np.full_like(pred["T_w"][: cfg.NPOINT], cfg.T_win)
+    plt.plot(x, y, label="t = 0.0 s")
+    for i in range(10):
+        y = pred["T_w"][cfg.NPOINT * i * 2 : cfg.NPOINT * (i * 2 + 1)]
+        plt.plot(x, y, label=f"t = {(i+1)*0.1:,.1f} s")
+    plt.xlabel("A")
+    plt.ylabel(r"$T_w$")
+    plt.legend()
+    plt.grid()
+    plt.savefig("T_w.png")
+    # plot the heat exchanger efficiency as a function of time.
+    plt.figure()
+    qm_min = np.min((visu_input["qm_h"][0], visu_input["qm_c"][0]))
+    eta = (
+        visu_input["qm_h"][0]
+        * (pred["T_h"][:: cfg.NPOINT] - pred["T_h"][cfg.NPOINT - 1 :: cfg.NPOINT])
+        / (
+            qm_min
+            * (pred["T_h"][:: cfg.NPOINT] - pred["T_c"][cfg.NPOINT - 1 :: cfg.NPOINT])
+        )
+    )
+    x = list(range(1, cfg.NTIME + 1))
+    plt.plot(x, eta)
+    plt.xlabel("time")
+    plt.ylabel(r"$\eta$")
+    plt.grid()
+    plt.savefig("eta.png")
+    error = np.square(eta[-1] - cfg.eta_true)
+    logger.info(
+        f"The L2 norm error between the actual heat exchanger efficiency and the predicted heat exchanger efficiency is {error}"
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="heat_exchanger.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

如图所示为不同时刻热边温度、冷边温度、壁面温度 \(T_h, T_c, T_w\) 随传热面积 \(A\) 的变化图像以及换热器效率 \(\eta\) 随时间的变化图像。

+
+说明 +

本案例只作为demo展示,尚未进行充分调优,下方部分展示结果可能与 OpenFOAM 存在一定差别。

+
+
+

T_h.png +

+
不同时刻热边温度 T_h 随传热面积 A 的变化图像
+
+
+

T_c.png +

+
不同时刻冷边温度 T_c 随传热面积 A 的变化图像
+
+
+

T_w.png +

+
不同时刻壁面温度 T_w 随传热面积 A 的变化图像
+
+
+

eta.png +

+
换热器效率随时间的变化图像
+
+

从图中可以看出:

+
    +
  • 热边温度在 \(A=1\) 处随时间的变化逐渐递减,冷边温度在 \(A=0\) 处随时间的变化逐渐递增;
  • +
  • 壁面温度在 \(A=1\) 处随时间的变化逐渐递减,在 \(A=0\) 处随时间的变化逐渐递增;
  • +
  • 换热器效率随时间的变化逐渐递增,在 \(t=21\) 时达到最大值。
  • +
+

同时我们可以假设热边质量流量和冷边质量流量相等,即 \(q_h=q_c\),定义传热单元数:

+
\[ +NTU = \dfrac{Ak}{(q_mc)_{min}}. +\]
+

对不同的传热单元数,我们可以分别计算对应的换热器效率,并画出换热器效率随传热单元数的变化图像,如图所示。

+
+

eta-1.png +

+
换热器效率随传热单元数的变化图像
+
+

从图中可以看出:换热器效率随传热单元数的变化逐渐递增,这也符合实际的换热器效率随传热单元数的变化规律。

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/heat_pinn/index.html b/zh/examples/heat_pinn/index.html new file mode 100644 index 0000000000..90318bc204 --- /dev/null +++ b/zh/examples/heat_pinn/index.html @@ -0,0 +1,4666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Heat_PINN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Heat_PINN

+
+
+
+
python heat_pinn.py
+
+
+
+
python heat_pinn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/heat_pinn/heat_pinn_pretrained.pdparams
+
+
+
+
python heat_pinn.py mode=export
+
+
+
+
python heat_pinn.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
heat_pinn_pretrained.pdparamsnorm MSE loss between the FDM and PINN is 1.30174e-03
+

1. 背景简介

+

热传导是自然界中的常见现象,广泛应用于工程、科学和技术领域。热传导问题在多个领域中都具有广泛的应用和重要性,对于提高能源效率、改进材料性能、促进科学研究和推动技术创新都起着至关重要的作用。因此了解和模拟传热过程对于设计和优化热传导设备、材料和系统至关重要。2D 定常热传导方程描述了稳态热传导过程,传统的求解方法涉及使用数值方法如有限元法或有限差分法,这些方法通常需要离散化领域并求解大规模矩阵系统。近年来,基于物理信息的神经网络(Physics-informed neural networks, PINN)逐渐成为求解偏微分方程的新方法。PINN 结合了神经网络的灵活性和对物理约束的建模能力,能够直接在连续领域中解决偏微分方程问题。

+

2. 问题定义

+

假设二维热传导方程中,每个位置 \((x,y)\) 上的温度 \(T\) 满足以下关系式:

+
\[ +\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2}=0, +\]
+

并且在以下区域内:

+
\[ +D = \{(x, y)|-1\leq{x}\leq{+1},-1\leq{y}\leq{+1}\}, +\]
+

具有以下边界条件:

+
\[ +\begin{cases} +T(-1, y) = 75.0 ^\circ{C}, \\ +T(+1, y) = 0.0 ^\circ{C}, \\ +T(x, -1) = 50.0 ^\circ{C}, \\ +T(x, +1) = 0.0 ^\circ{C}. +\end{cases} +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在二维热传导问题中,每一个已知的坐标点 \((x, y)\) 都有对应的待求解的未知量 \(T\) +,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y)\)\(u\) 的映射函数 \(f: \mathbb{R}^2 \to \mathbb{R}^1\) ,即:

+
\[ +u = f(x, y), +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
plt.title("PINN")
+plt.xlabel("x")
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("x", "y"),输出变量名是 "u",这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数和激活函数,我们就实例化出了一个拥有 9 层隐藏神经元、每层神经元数为 20 以及激活函数为 tanh 的神经网络模型 model

+

3.2 方程构建

+

由于二维热传导方程使用的是 Laplace 方程的 2 维形式,因此可以直接使用 PaddleScience 内置的 Laplace,指定该类的参数 dim 为 2。

+
plt.tight_layout()
+plt.axis("square")
+
+

3.3 计算域构建

+

本文中二维热传导问题作用在以 (-1.0, -1.0), (1.0, 1.0) 为对角线的二维矩形区域, +因此可以直接使用 PaddleScience 内置的空间几何 Rectangle 作为计算域。

+
plt.subplot(2, 1, 2)
+plt.pcolormesh(x, y, fdm_output, cmap="magma")
+
+

3.4 约束构建

+

在本案例中,我们使用了两种约束条件在计算域中指导模型的训练分别是作用于采样点上的热传导方程约束和作用于边界点上的约束。

+

在定义约束之前,需要给每一种约束指定采样点个数,表示每一种约束在其对应计算域内采样数据的数量,以及通用的采样配置。

+
49
+50
+51
+52
+53
+54
plt.close()
+
+frames_val = np.array([-0.75, -0.5, -0.25, 0.0, +0.25, +0.5, +0.75])
+frames = [*map(int, (frames_val + 1) / 2 * (N_EVAL - 1))]
+height = 3
+plt.figure("", figsize=(len(frames) * height, 2 * height))
+
+

3.4.1 内部点约束

+

以作用在内部点上的 InteriorConstraint 为例,代码如下:

+
55
+56
+57
+58
+59
+60
+61
+62
for i, var_index in enumerate(frames):
+    plt.subplot(2, len(frames), i + 1)
+    plt.title(f"y = {frames_val[i]:.2f}")
+    plt.plot(
+        x[:, var_index],
+        pinn_output[:, var_index] * 75.0,
+        "r--",
+        lw=4.0,
+
+

InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["Laplace"].equations

+

第二个参数是约束变量的目标值,根据热传导方程的定义,我们希望 Laplace 方程产生的结果全为 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["rect"] 即可;

+

第四个参数是在计算域上的采样配置,此处我们使用全量数据点训练,因此 dataset 字段设置为 "IterableNamedArrayDataset" 且 iters_per_epoch 也设置为 1,采样点数 batch_size 设为 NPOINT_PDE(表示99x99的采样网格);

+

第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求平均;

+

第六个参数是计算 loss 的时候的该约束的权值大小,参考PINN论文,这里我们设置为 1;

+

第七个参数是选择是否在计算域上进行等间隔采样,此处我们选择开启等间隔采样,这样能让训练点均匀分布在计算域上,有利于训练收敛;

+

第八个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

+

3.4.2 边界约束

+

同理,我们还需要构建矩形的四个边界的约束。但与构建 InteriorConstraint 约束不同的是,由于作用区域是边界,因此我们使用 BoundaryConstraint 类,代码如下:

+
            label="pinn",
+        )
+        plt.plot(x[:, var_index], fdm_output[:, var_index], "b", lw=2.0, label="FDM")
+        plt.ylim(0.0, 100.0)
+        plt.xlim(-1.0, +1.0)
+        plt.xlabel("x")
+        plt.ylabel("T")
+        plt.tight_layout()
+        plt.legend()
+
+    for i, var_index in enumerate(frames):
+        plt.subplot(2, len(frames), len(frames) + i + 1)
+        plt.title(f"x = {frames_val[i]:.2f}")
+        plt.plot(
+            y[var_index, :],
+            pinn_output[var_index, :] * 75.0,
+            "r--",
+            lw=4.0,
+            label="pinn",
+        )
+        plt.plot(y[var_index, :], fdm_output[var_index, :], "b", lw=2.0, label="FDM")
+        plt.ylim(0.0, 100.0)
+        plt.xlim(-1.0, +1.0)
+        plt.xlabel("y")
+        plt.ylabel("T")
+        plt.tight_layout()
+        plt.legend()
+
+    plt.savefig(osp.join(cfg.output_dir, "profiles.png"))
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # set output directory
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+

BoundaryConstraint 类第一个参数表示我们直接对网络模型的输出结果 out["u"] 作为程序运行时的约束对象;

+

第二个参数是指我们约束对象的真值为多少,该问题中边界条件为 Dirichlet 边界条件,也就是该边界条件直接描述物理系统边界上的物理量,给定一个固定的边界值,具体的边界条件值已在 2. 问题定义 中给出;

+

BoundaryConstraint 类其他参数的含义与 InteriorConstraint 基本一致,这里不再介绍。

+

在微分方程约束和边界约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# set equation
+equation = {"heat": ppsci.equation.Laplace(dim=2)}
+
+# set geometry
+geom = {"rect": ppsci.geometry.Rectangle((-1.0, -1.0), (1.0, 1.0))}
+
+# set train dataloader config
+
+

3.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器,并设置学习率为 0.0005。

+
36
+37
+38
+39
+40
+41
# training settings
+TRAIN:
+  epochs: 1000
+  iters_per_epoch: 1
+  save_freq: 20
+  learning_rate: 5.0e-4
+
+
"dataset": "IterableNamedArrayDataset",
+"iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+
+

3.6 模型训练

+

完成上述设置之后,只需要将所有上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# set constraint
+NPOINT_PDE = 99**2
+NPOINT_TOP = 25
+NPOINT_BOTTOM = 25
+NPOINT_LEFT = 25
+NPOINT_RIGHT = 25
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["heat"].equations,
+    {"laplace": 0},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_PDE},
+    ppsci.loss.MSELoss("mean"),
+    evenly=True,
+    name="EQ",
+)
+bc_top = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+
+

3.7 模型评估、可视化

+

模型训练完成之后就需要进行与正式 FDM 方法计算出来的结果进行对比,这里我们使用了 geom["rect"].sample_interior 采样出测试所需要的坐标数据。 +然后,再将采样出来的坐标数据输入到模型中,得到模型的预测结果,最后将预测结果与 FDM 结果进行对比,得到模型的误差。

+
    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_TOP},
+    ppsci.loss.MSELoss("mean"),
+    weight_dict={"u": cfg.TRAIN.weight.bc_top},
+    criteria=lambda x, y: np.isclose(y, 1),
+    name="BC_top",
+)
+bc_bottom = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+    {"u": 50 / 75},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_BOTTOM},
+    ppsci.loss.MSELoss("mean"),
+    weight_dict={"u": cfg.TRAIN.weight.bc_bottom},
+    criteria=lambda x, y: np.isclose(y, -1),
+    name="BC_bottom",
+)
+bc_left = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+    {"u": 1},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_LEFT},
+    ppsci.loss.MSELoss("mean"),
+    weight_dict={"u": cfg.TRAIN.weight.bc_left},
+    criteria=lambda x, y: np.isclose(x, -1),
+    name="BC_left",
+)
+bc_right = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+    {"u": 0},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_RIGHT},
+    ppsci.loss.MSELoss("mean"),
+    weight_dict={"u": cfg.TRAIN.weight.bc_right},
+    criteria=lambda x, y: np.isclose(x, 1),
+    name="BC_right",
+)
+# wrap constraints together
+constraint = {
+    pde_constraint.name: pde_constraint,
+    bc_top.name: bc_top,
+    bc_bottom.name: bc_bottom,
+    bc_left.name: bc_left,
+    bc_right.name: bc_right,
+}
+
+# set optimizer
+optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    log_freq=cfg.log_freq,
+    seed=cfg.seed,
+    equation=equation,
+    geom=geom,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+# train model
+solver.train()
+
+# begin eval
+N_EVAL = 100
+input_data = geom["rect"].sample_interior(N_EVAL**2, evenly=True)
+pinn_output = solver.predict(input_data, return_numpy=True)["u"].reshape(
+    N_EVAL, N_EVAL
+)
+fdm_output = fdm.solve(N_EVAL, 1).T
+
+

4. 完整代码

+
heat_pinn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import fdm
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def plot(input_data, N_EVAL, pinn_output, fdm_output, cfg):
+    x = input_data["x"].reshape(N_EVAL, N_EVAL)
+    y = input_data["y"].reshape(N_EVAL, N_EVAL)
+
+    plt.subplot(2, 1, 1)
+    plt.pcolormesh(x, y, pinn_output * 75.0, cmap="magma")
+    plt.colorbar()
+    plt.title("PINN")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    plt.tight_layout()
+    plt.axis("square")
+
+    plt.subplot(2, 1, 2)
+    plt.pcolormesh(x, y, fdm_output, cmap="magma")
+    plt.colorbar()
+    plt.xlabel("x")
+    plt.ylabel("y")
+    plt.title("FDM")
+    plt.tight_layout()
+    plt.axis("square")
+    plt.savefig(osp.join(cfg.output_dir, "pinn_fdm_comparison.png"))
+    plt.close()
+
+    frames_val = np.array([-0.75, -0.5, -0.25, 0.0, +0.25, +0.5, +0.75])
+    frames = [*map(int, (frames_val + 1) / 2 * (N_EVAL - 1))]
+    height = 3
+    plt.figure("", figsize=(len(frames) * height, 2 * height))
+
+    for i, var_index in enumerate(frames):
+        plt.subplot(2, len(frames), i + 1)
+        plt.title(f"y = {frames_val[i]:.2f}")
+        plt.plot(
+            x[:, var_index],
+            pinn_output[:, var_index] * 75.0,
+            "r--",
+            lw=4.0,
+            label="pinn",
+        )
+        plt.plot(x[:, var_index], fdm_output[:, var_index], "b", lw=2.0, label="FDM")
+        plt.ylim(0.0, 100.0)
+        plt.xlim(-1.0, +1.0)
+        plt.xlabel("x")
+        plt.ylabel("T")
+        plt.tight_layout()
+        plt.legend()
+
+    for i, var_index in enumerate(frames):
+        plt.subplot(2, len(frames), len(frames) + i + 1)
+        plt.title(f"x = {frames_val[i]:.2f}")
+        plt.plot(
+            y[var_index, :],
+            pinn_output[var_index, :] * 75.0,
+            "r--",
+            lw=4.0,
+            label="pinn",
+        )
+        plt.plot(y[var_index, :], fdm_output[var_index, :], "b", lw=2.0, label="FDM")
+        plt.ylim(0.0, 100.0)
+        plt.xlim(-1.0, +1.0)
+        plt.xlabel("y")
+        plt.ylabel("T")
+        plt.tight_layout()
+        plt.legend()
+
+    plt.savefig(osp.join(cfg.output_dir, "profiles.png"))
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # set output directory
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"heat": ppsci.equation.Laplace(dim=2)}
+
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((-1.0, -1.0), (1.0, 1.0))}
+
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": "IterableNamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+
+    # set constraint
+    NPOINT_PDE = 99**2
+    NPOINT_TOP = 25
+    NPOINT_BOTTOM = 25
+    NPOINT_LEFT = 25
+    NPOINT_RIGHT = 25
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["heat"].equations,
+        {"laplace": 0},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_PDE},
+        ppsci.loss.MSELoss("mean"),
+        evenly=True,
+        name="EQ",
+    )
+    bc_top = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": 0},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_TOP},
+        ppsci.loss.MSELoss("mean"),
+        weight_dict={"u": cfg.TRAIN.weight.bc_top},
+        criteria=lambda x, y: np.isclose(y, 1),
+        name="BC_top",
+    )
+    bc_bottom = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": 50 / 75},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_BOTTOM},
+        ppsci.loss.MSELoss("mean"),
+        weight_dict={"u": cfg.TRAIN.weight.bc_bottom},
+        criteria=lambda x, y: np.isclose(y, -1),
+        name="BC_bottom",
+    )
+    bc_left = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": 1},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_LEFT},
+        ppsci.loss.MSELoss("mean"),
+        weight_dict={"u": cfg.TRAIN.weight.bc_left},
+        criteria=lambda x, y: np.isclose(x, -1),
+        name="BC_left",
+    )
+    bc_right = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": 0},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_RIGHT},
+        ppsci.loss.MSELoss("mean"),
+        weight_dict={"u": cfg.TRAIN.weight.bc_right},
+        criteria=lambda x, y: np.isclose(x, 1),
+        name="BC_right",
+    )
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        bc_top.name: bc_top,
+        bc_bottom.name: bc_bottom,
+        bc_left.name: bc_left,
+        bc_right.name: bc_right,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+    # train model
+    solver.train()
+
+    # begin eval
+    N_EVAL = 100
+    input_data = geom["rect"].sample_interior(N_EVAL**2, evenly=True)
+    pinn_output = solver.predict(input_data, return_numpy=True)["u"].reshape(
+        N_EVAL, N_EVAL
+    )
+    fdm_output = fdm.solve(N_EVAL, 1).T
+    mse_loss = np.mean(np.square(pinn_output - (fdm_output / 75.0)))
+    logger.info(f"The norm MSE loss between the FDM and PINN is {mse_loss}")
+    plot(input_data, N_EVAL, pinn_output, fdm_output, cfg)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # set output directory
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((-1.0, -1.0), (1.0, 1.0))}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    # begin eval
+    N_EVAL = 100
+    input_data = geom["rect"].sample_interior(N_EVAL**2, evenly=True)
+    pinn_output = solver.predict(input_data, no_grad=True, return_numpy=True)[
+        "u"
+    ].reshape(N_EVAL, N_EVAL)
+    fdm_output = fdm.solve(N_EVAL, 1).T
+    mse_loss = np.mean(np.square(pinn_output - (fdm_output / 75.0)))
+    logger.info(f"The norm MSE loss between the FDM and PINN is {mse_loss:.5e}")
+    plot(input_data, N_EVAL, pinn_output, fdm_output, cfg)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        cfg=cfg,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+    # set geometry
+    geom = {"rect": ppsci.geometry.Rectangle((-1.0, -1.0), (1.0, 1.0))}
+    # begin eval
+    N_EVAL = 100
+    input_data = geom["rect"].sample_interior(N_EVAL**2, evenly=True)
+    output_data = predictor.predict(
+        {key: input_data[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_data = {
+        store_key: output_data[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_data.keys())
+    }["u"].reshape(N_EVAL, N_EVAL)
+    fdm_output = fdm.solve(N_EVAL, 1).T
+    mse_loss = np.mean(np.square(output_data - (fdm_output / 75.0)))
+    logger.info(f"The norm MSE loss between the FDM and PINN is {mse_loss:.5e}")
+    plot(input_data, N_EVAL, output_data, fdm_output, cfg)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="heat_pinn.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+
+

T_comparison +

+
上:PINN 计算结果,下:FDM 计算结果 +
+
+

上图展示了使用 PINN 和 FDM 方法分别计算出的温度分布图,从中可以看出它们之间的结果非常接近。此外,PINN 和 FDM 两者之间的均方误差(MSE Loss)仅为 0.0013。综合考虑图形和数值结果,可以得出结论,PINN 能够有效地解决本案例的传热问题。

+
+

profile +

+
上:PINN 与FDM 在 x 方向 T 结果对比,下:PINN 与 FDM 在 y 方向 T 结果对比 +
+
+

上图分别为温度 \(T\) 的横截线图( \(y=\{-0.75,-0.50,-0.25,0.00,0.25,0.50,0.75\}\) )和纵截线图( \(x=\{-0.75,-0.50,-0.25,0.00,0.25,0.50,0.75\}\) ),可以看到 PINN 与 FDM 方法的计算结果基本一致。

+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/hpinns/index.html b/zh/examples/hpinns/index.html new file mode 100644 index 0000000000..19e42a7bca --- /dev/null +++ b/zh/examples/hpinns/index.html @@ -0,0 +1,6867 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hPINNs - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

hPINNs(PINN with hard constraints)

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat --create-dirs -o ./datasets/hpinns_holo_train.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat --create-dirs -o ./datasets/hpinns_holo_valid.mat
+python holography.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat --create-dirs -o ./datasets/hpinns_holo_train.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat --create-dirs -o ./datasets/hpinns_holo_valid.mat
+python holography.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/hPINNs/hpinns_pretrained.pdparams
+
+
+
+
python holography.py mode=export
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat --create-dirs -o ./datasets/hpinns_holo_train.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat --create-dirs -o ./datasets/hpinns_holo_valid.mat
+python holography.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
hpinns_pretrained.pdparamsloss(opt_sup): 0.05352
MSE.eval_metric(opt_sup): 0.00002
loss(val_sup): 0.02205
MSE.eval_metric(val_sup): 0.00001
+

1. 背景简介

+

求解偏微分方程(PDE) 是一类基础的物理问题,在过去几十年里,以有限差分(FDM)、有限体积(FVM)、有限元(FEM)为代表的多种偏微分方程组数值解法趋于成熟。随着人工智能技术的高速发展,利用深度学习求解偏微分方程成为新的研究趋势。PINNs(Physics-informed neural networks) 是一种加入物理约束的深度学习网络,因此与纯数据驱动的神经网络学习相比,PINNs 可以用更少的数据样本学习到更具泛化能力的模型,其应用范围包括但不限于流体力学、热传导、电磁场、量子力学等领域。

+

传统的 PINNs 网络中的约束都是软约束,即 PDE(偏微分方程) 作为 loss 项参与网络训练。而本案例 hPINNs 通过修改网络输出的方法,将约束严格地加入网络结构中,形成一种更有效的硬约束。

+

同时 hPINNs 设计了不同的约束组合,进行了软约束、带正则化的硬约束和应用增强的拉格朗日硬约束 3 种条件下的实验。本文档主要针对应用增强的拉格朗日方法的硬约束进行说明,但完整代码中可以通过 train_mode 参数来切换三种训练模式。

+

本问题可参考 AI Studio题目.

+

2. 问题定义

+

本问题使用 hPINNs 解决基于傅立叶光学的全息领域 (holography) 的问题,旨在设计散射板的介电常数图,这种方法使得介电常数图散射光线的传播强度具备目标函数的形状。

+

objective 函数:

+
\[ +\begin{aligned} +\mathcal{J}(E) &= \dfrac{1}{Area(\Omega_3)} \left\| |E(x,y)|^2-f(x,y)\right\|^2_{2,\Omega_3} \\ +&= \dfrac{1}{Area(\Omega_3)} \int_{\Omega_3} (|E(x,y)|^2-f(x,y))^2 {\rm d}x {\rm d}y +\end{aligned} +\]
+

其中E为电场强度:\(\vert E\vert^2 = (\mathfrak{R} [E])^2+(\mathfrak{I} [E])^2\)

+

target 函数:

+
\[ f(x,y) = +\begin{cases} +\begin{aligned} +& 1, \ (x,y) \in [-0.5,0.5] \cap [1,2]\\ +& 0, \ otherwise +\end{aligned} +\end{cases} +\]
+

PDE公式:

+
\[ +\nabla^2 E + \varepsilon \omega^2 E = -i \omega \mathcal{J} +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集为处理好的 holography 数据集,包含训练、测试数据的 \(x, y\) 以及表征 optimizer area 数据与全区域数据分界的值 \(bound\),以字典的形式存储在 .mat 文件中。

+

运行本问题代码前请按照下方命令下载 训练数据集验证数据集

+
wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_train.mat
+wget -nc -P ./datasets/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/hPINNs/hpinns_holo_valid.mat
+
+

3.2 模型构建

+

holograpy 问题的模型结构图为:

+
+

holography-arch +

+
holography 问题的 hPINNs 网络模型
+
+

在 holography 问题中,应用 PMLs(perfectly matched layers) 方法后,PDE公式变为:

+
\[ +\dfrac{1}{1+i \dfrac{\sigma_x\left(x\right)}{\omega}} \dfrac{\partial}{\partial x} \left(\dfrac{1}{1+i \dfrac{\sigma_x\left(x\right)}{\omega}} \dfrac{\partial E}{\partial x}\right)+\dfrac{1}{1+i \dfrac{\sigma_y\left(y\right)}{\omega}} \dfrac{\partial}{\partial y} \left(\dfrac{1}{1+i \dfrac{\sigma_y\left(y\right)}{\omega}} \dfrac{\partial E}{\partial y}\right) + \varepsilon \omega^2 E = -i \omega \mathcal{J} +\]
+

PMLs 方法请参考 相关论文

+

本问题中频率 \(\omega\) 为常量 \(\dfrac{2\pi}{\mathcal{P}}\)\(\mathcal{P}\) 为Period),待求解的未知量 \(E\) 与位置参数 \((x, y)\) 相关,在本例中,介电常数 \(\varepsilon\) 同样为未知量, \(\sigma_x(x)\)\(\sigma_y(y)\) 为由 PMLs 得到的,分别与 \(x, y\) 相关的变量。我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y)\)\((E, \varepsilon)\) 的映射函数 \(f: \mathbb{R}^2 \to \mathbb{R}^2\) ,但如上图所示的网络结构,本问题中将 \(E\) 按照实部和虚部分为两个部分 \((\mathfrak{R} [E],\mathfrak{I} [E])\),且使用 3 个并行的 MLP 网络分别对 \((\mathfrak{R} [E], \mathfrak{I} [E], \varepsilon)\) 进行映射,映射函数 \(f_i: \mathbb{R}^2 \to \mathbb{R}^1\) ,即:

+
\[ +\mathfrak{R} [E] = f_1(x,y), \ \mathfrak{R} [E] = f_2(x,y), \ \varepsilon = f_3(x,y) +\]
+

上式中 \(f_1,f_2,f_3\) 分别为一个 MLP 模型,三者共同构成了一个 Model List,用 PaddleScience 代码表示如下

+
42
+43
+44
model_re = ppsci.arch.MLP(**cfg.MODEL.re_net)
+model_im = ppsci.arch.MLP(**cfg.MODEL.im_net)
+model_eps = ppsci.arch.MLP(**cfg.MODEL.eps_net)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("x_cos_1","x_sin_1",...,"x_cos_6","x_sin_6","y","y_cos_1","y_sin_1") ,输出变量名分别是 ("e_re",), ("e_im",), ("eps",)。 +注意到这里的输入变量远远多于 \((x, y)\) 这两个变量,这是因为如上图所示,模型的输入实际上是 \((x, y)\) 傅立叶展开的项而不是它们本身。而数据集中提供的训练数据为 \((x, y)\) 值,这也就意味着我们需要对输入进行 transform。同时如上图所示,由于硬约束的存在,模型的输出变量名也不是最终输出,因此也需要对输出进行 transform。

+

3.3 transform构建

+

输入的 transform 为变量 \((x, y)\)\((\cos(\omega x),\sin(\omega x),...,\cos(6 \omega x),\sin(6 \omega x),y,\cos(\omega y),\sin(\omega y))\) 的变换,输出 transform 分别为对 \((\mathfrak{R} [E], \mathfrak{I} [E], \varepsilon)\) 的硬约束,代码如下

+
49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
# transform
+def transform_in(input):
+    # Periodic BC in x
+    P = BOX[1][0] - BOX[0][0] + 2 * DPML
+    w = 2 * np.pi / P
+    x, y = input["x"], input["y"]
+    input_transformed = {}
+    for t in range(1, 7):
+        input_transformed[f"x_cos_{t}"] = paddle.cos(t * w * x)
+        input_transformed[f"x_sin_{t}"] = paddle.sin(t * w * x)
+    input_transformed["y"] = y
+    input_transformed["y_cos_1"] = paddle.cos(OMEGA * y)
+    input_transformed["y_sin_1"] = paddle.sin(OMEGA * y)
+
+    return input_transformed
+
+
+def transform_out_all(input, var):
+    y = input["y"]
+    # Zero Dirichlet BC
+    a, b = BOX[0][1] - DPML, BOX[1][1] + DPML
+    t = (1 - paddle.exp(a - y)) * (1 - paddle.exp(y - b))
+    return t * var
+
+
+def transform_out_real_part(input, out):
+    re = out["e_re"]
+    trans_out = transform_out_all(input, re)
+    return {"e_real": trans_out}
+
+
+def transform_out_imaginary_part(input, out):
+    im = out["e_im"]
+    trans_out = transform_out_all(input, im)
+    return {"e_imaginary": trans_out}
+
+
+def transform_out_epsilon(input, out):
+    eps = out["eps"]
+    # 1 <= eps <= 12
+    eps = F.sigmoid(eps) * 11 + 1
+    return {"epsilon": eps}
+
+

需要对每个 MLP 模型分别注册相应的 transform ,然后将 3 个 MLP 模型组成 Model List

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
# register transform
+model_re.register_input_transform(func_module.transform_in)
+model_im.register_input_transform(func_module.transform_in)
+model_eps.register_input_transform(func_module.transform_in)
+
+model_re.register_output_transform(func_module.transform_out_real_part)
+model_im.register_output_transform(func_module.transform_out_imaginary_part)
+model_eps.register_output_transform(func_module.transform_out_epsilon)
+
+model_list = ppsci.arch.ModelList((model_re, model_im, model_eps))
+
+

这样我们就实例化出了一个拥有 3 个 MLP 模型,每个 MLP 包含 4 层隐藏神经元,每层神经元数为 48,使用 "tanh" 作为激活函数,并包含输入输出 transform 的神经网络模型 model list

+

3.4 参数和超参数设定

+

我们需要指定问题相关的参数,如通过 train_mode 参数指定应用增强的拉格朗日方法的硬约束进行训练

+
35
+36
+37
+38
+39
+40
# open FLAG for higher order differential operator
+paddle.framework.core.set_prim_eager_enabled(True)
+
+ppsci.utils.misc.set_random_seed(cfg.seed)
+# initialize logger
+logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+
46
+47
+48
# initialize params
+func_module.train_mode = cfg.TRAIN_MODE
+loss_log_obj = []
+
+
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
# define constants
+BOX = np.array([[-2, -2], [2, 3]])
+DPML = 1
+OMEGA = 2 * np.pi
+SIGMA0 = -np.log(1e-20) / (4 * DPML**3 / 3)
+l_BOX = BOX + np.array([[-DPML, -DPML], [DPML, DPML]])
+beta = 2.0
+mu = 2
+
+# define variables which will be updated during training
+lambda_re: np.ndarray = None
+lambda_im: np.ndarray = None
+loss_weight: List[float] = None
+train_mode: str = None
+
+# define log variables for plotting
+loss_log = []  # record all losses, [pde, lag, obj]
+loss_obj = 0.0  # record last objective loss of each k
+lambda_log = []  # record all lambdas
+
+

由于应用了增强的拉格朗日方法,参数 \(\mu\)\(\lambda\) 不是常量,而是随训练轮次 \(k\) 改变,此时 \(\beta\) 为改变的系数,即每轮训练

+

\(\mu_k = \beta \mu_{k-1}\), \(\lambda_k = \beta \lambda_{k-1}\)

+

同时需要指定训练轮数和学习率等超参数

+
53
+54
+55
+56
+57
+58
+59
+60
+61
    activation: "tanh"
+
+# training settings
+TRAIN:
+  epochs: 20000
+  iters_per_epoch: 1
+  eval_during_train: false
+  learning_rate: 0.001
+  max_iter: 15000
+
+

3.5 优化器构建

+

训练分为两个阶段,先使用 Adam 优化器进行大致训练,再使用 LBFGS 优化器逼近最优点,因此需要两个优化器,这也对应了上一部分超参数中的两种 EPOCHS

+
62
+63
+64
optimizer_adam = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(
+    (model_re, model_im, model_eps)
+)
+
+
optimizer_lbfgs = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(
+    (model_re, model_im, model_eps)
+)
+
+

3.6 约束构建

+

本问题采用无监督学习的方式,约束为结果需要满足PDE公式。

+

虽然我们不是以监督学习方式进行训练,但此处仍然可以采用监督约束 SupervisedConstraint,在定义约束之前,需要给监督约束指定文件路径等数据读取配置,因为数据集中没有标签数据,因此在数据读取时我们需要使用训练数据充当标签数据,并注意在之后不要使用这部分“假的”标签数据。

+
"alias_dict": {
+    "e_real": "x",
+    "e_imaginary": "x",
+    "epsilon": "x",
+    **{k: "x" for k in label_keys_derivative},
+},
+
+

如上,所有输出的标签都会读取输入 x 的值。

+

下面是约束等具体内容,要注意上述提到的给定“假的”标签数据:

+
# manually build constraint(s)
+label_keys = ("x", "y", "bound", "e_real", "e_imaginary", "epsilon")
+label_keys_derivative = (
+    "de_re_x",
+    "de_re_y",
+    "de_re_xx",
+    "de_re_yy",
+    "de_im_x",
+    "de_im_y",
+    "de_im_xx",
+    "de_im_yy",
+)
+output_expr = {
+    "x": lambda out: out["x"],
+    "y": lambda out: out["y"],
+    "bound": lambda out: out["bound"],
+    "e_real": lambda out: out["e_real"],
+    "e_imaginary": lambda out: out["e_imaginary"],
+    "epsilon": lambda out: out["epsilon"],
+    "de_re_x": lambda out: jacobian(out["e_real"], out["x"]),
+    "de_re_y": lambda out: jacobian(out["e_real"], out["y"]),
+    "de_re_xx": lambda out: hessian(out["e_real"], out["x"]),
+    "de_re_yy": lambda out: hessian(out["e_real"], out["y"]),
+    "de_im_x": lambda out: jacobian(out["e_imaginary"], out["x"]),
+    "de_im_y": lambda out: jacobian(out["e_imaginary"], out["y"]),
+    "de_im_xx": lambda out: hessian(out["e_imaginary"], out["x"]),
+    "de_im_yy": lambda out: hessian(out["e_imaginary"], out["y"]),
+}
+
+sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("x", "y", "bound"),
+            "label_keys": label_keys + label_keys_derivative,
+            "alias_dict": {
+                "e_real": "x",
+                "e_imaginary": "x",
+                "epsilon": "x",
+                **{k: "x" for k in label_keys_derivative},
+            },
+        },
+    },
+    ppsci.loss.FunctionalLoss(func_module.pde_loss_fun),
+    output_expr,
+    name="sup_constraint_pde",
+)
+sup_constraint_obj = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH,
+            "input_keys": ("x", "y", "bound"),
+            "label_keys": label_keys,
+            "alias_dict": {"e_real": "x", "e_imaginary": "x", "epsilon": "x"},
+        },
+    },
+    ppsci.loss.FunctionalLoss(func_module.obj_loss_fun),
+    {key: lambda out, k=key: out[k] for key in label_keys},
+    name="sup_constraint_obj",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,其中 “dataset” 字段表示使用的训练数据集信息,各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 "IterableMatDataset" 表示不分 batch 顺序读取的 .mat 类型的数据集;
  2. +
  3. file_path: 数据集文件路径;
  4. +
  5. input_keys: 输入变量名;
  6. +
  7. label_keys: 标签变量名;
  8. +
  9. alias_dict: 变量别名。
  10. +
+

第二个参数是损失函数,此处的 FunctionalLoss 为 PaddleScience 预留的自定义 loss 函数类,该类支持编写代码时自定义 loss 的计算方法,而不是使用诸如 MSE 等现有方法,本问题中由于存在多个 loss 项,因此需要定义多个 loss 计算函数,这也是需要构建多个约束的原因。自定义 loss 函数代码请参考 自定义 loss 和 metric

+

第三个参数是方程表达式,用于描述如何计算约束目标,此处填入 output_expr,计算后的值将会按照指定名称存入输出列表中,从而保证 loss 计算时可以使用这些值。

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
constraint = {
+    sup_constraint_pde.name: sup_constraint_pde,
+    sup_constraint_obj.name: sup_constraint_obj,
+}
+
+

3.7 评估器构建

+

与约束同理,虽然本问题使用无监督学习,但仍可以使用 ppsci.validate.SupervisedValidator 构建评估器。本问题存在两个采样点区域,一个是较大的完整定义区域,另一个是定义域中的一块 objective 区域,评估器分别对这两个区域进行评估,因此需要构建两个评估器。opt对应 objective 区域,val 对应整个定义域。

+
# manually build validator
+sup_validator_opt = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_VALID,
+            "input_keys": ("x", "y", "bound"),
+            "label_keys": label_keys + label_keys_derivative,
+            "alias_dict": {
+                "x": "x_opt",
+                "y": "y_opt",
+                "e_real": "x_opt",
+                "e_imaginary": "x_opt",
+                "epsilon": "x_opt",
+                **{k: "x_opt" for k in label_keys_derivative},
+            },
+        },
+    },
+    ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+    output_expr,
+    {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+    name="opt_sup",
+)
+sup_validator_val = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATASET_PATH_VALID,
+            "input_keys": ("x", "y", "bound"),
+            "label_keys": label_keys + label_keys_derivative,
+            "alias_dict": {
+                "x": "x_val",
+                "y": "y_val",
+                "e_real": "x_val",
+                "e_imaginary": "x_val",
+                "epsilon": "x_val",
+                **{k: "x_val" for k in label_keys_derivative},
+            },
+        },
+    },
+    ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+    output_expr,
+    {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+    name="val_sup",
+)
+validator = {
+    sup_validator_opt.name: sup_validator_opt,
+    sup_validator_val.name: sup_validator_val,
+}
+
+

评价指标 metricFunctionalMetric,这是 PaddleScience 预留的自定义 metric 函数类,该类支持编写代码时自定义 metric 的计算方法,而不是使用诸如 MSEL2 等现有方法。自定义 metric 函数代码请参考下一部分 自定义 loss 和 metric

+

其余配置与 约束构建 的设置类似。

+

3.8 自定义 loss 和 metric

+

由于本问题采用无监督学习,数据中不存在标签数据,loss 和 metric 根据 PDE 计算得到,因此需要自定义 loss 和 metric。方法为先定义相关函数,再将函数名作为参数传给 FunctionalLossFunctionalMetric

+

需要注意自定义 loss 和 metric 函数的输入输出参数需要与 PaddleScience 中如 MSE 等其他函数保持一致,即输入为模型输出 output_dict 等字典变量,loss 函数输出为 loss 值 paddle.Tensor,metric 函数输出为字典 Dict[str, paddle.Tensor]

+
def pde_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute pde loss and lagrangian loss.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: PDE loss (and lagrangian loss if using Augmented Lagrangian method).
+    """
+    global loss_log
+    bound = int(output_dict["bound"])
+    loss_re, loss_im = compute_real_and_imaginary_loss(output_dict)
+    loss_re = loss_re[bound:]
+    loss_im = loss_im[bound:]
+
+    loss_eqs1 = paddle.mean(loss_re**2)
+    loss_eqs2 = paddle.mean(loss_im**2)
+    # augmented_Lagrangian
+    if lambda_im is None:
+        init_lambda(output_dict, bound)
+    loss_lag1 = paddle.mean(loss_re * lambda_re)
+    loss_lag2 = paddle.mean(loss_im * lambda_im)
+
+    losses = (
+        loss_weight[0] * loss_eqs1
+        + loss_weight[1] * loss_eqs2
+        + loss_weight[2] * loss_lag1
+        + loss_weight[3] * loss_lag2
+    )
+    loss_log.append(float(loss_eqs1 + loss_eqs2))  # for plotting
+    loss_log.append(float(loss_lag1 + loss_lag2))  # for plotting
+    return {"pde": losses}
+
+
+def obj_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute objective loss.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: Objective loss.
+    """
+    global loss_log, loss_obj
+    x, y = output_dict["x"], output_dict["y"]
+    bound = int(output_dict["bound"])
+    e_re = output_dict["e_real"]
+    e_im = output_dict["e_imaginary"]
+
+    f1 = paddle.heaviside((x + 0.5) * (0.5 - x), paddle.to_tensor(0.5))
+    f2 = paddle.heaviside((y - 1) * (2 - y), paddle.to_tensor(0.5))
+    j = e_re[:bound] ** 2 + e_im[:bound] ** 2 - f1[:bound] * f2[:bound]
+    loss_opt_area = paddle.mean(j**2)
+
+    if lambda_im is None:
+        init_lambda(output_dict, bound)
+    losses = loss_weight[4] * loss_opt_area
+    loss_log.append(float(loss_opt_area))  # for plotting
+    loss_obj = float(loss_opt_area)  # for plotting
+    return {"obj": losses}
+
+
+def eval_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute objective loss for evaluation.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: Objective loss.
+    """
+    x, y = output_dict["x"], output_dict["y"]
+    e_re = output_dict["e_real"]
+    e_im = output_dict["e_imaginary"]
+
+    f1 = paddle.heaviside((x + 0.5) * (0.5 - x), paddle.to_tensor(0.5))
+    f2 = paddle.heaviside((y - 1) * (2 - y), paddle.to_tensor(0.5))
+    j = e_re**2 + e_im**2 - f1 * f2
+    losses = paddle.mean(j**2)
+
+    return {"eval": losses}
+
+
def eval_metric_fun(
+    output_dict: Dict[str, paddle.Tensor], *args
+) -> Dict[str, paddle.Tensor]:
+    """Compute metric for evaluation.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        Dict[str, paddle.Tensor]: MSE metric.
+    """
+    loss_re, loss_im = compute_real_and_imaginary_loss(output_dict)
+    eps_opt = paddle.concat([loss_re, loss_im], axis=-1)
+    metric = paddle.mean(eps_opt**2)
+
+    metric_dict = {"eval_metric": metric}
+    return metric_dict
+
+

3.9 模型训练、评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model_list,
+    constraint,
+    cfg.output_dir,
+    optimizer_adam,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    validator=validator,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

由于本问题存在多种训练模式,根据每个模式的不同,将进行 \([2,1+k]\) 次完整的训练、评估,具体代码请参考 完整代码 中 holography.py 文件。

+

3.10 可视化

+

PaddleScience 中提供了可视化器,但由于本问题图片数量较多且较为复杂,代码中自定义了可视化函数,调用自定义函数即可实现可视化

+
################# plotting ###################
+# log of loss
+loss_log = np.array(func_module.loss_log).reshape(-1, 3)
+
+plot_module.set_params(
+    cfg.TRAIN_MODE, cfg.output_dir, cfg.DATASET_PATH, cfg.DATASET_PATH_VALID
+)
+plot_module.plot_6a(loss_log)
+if cfg.TRAIN_MODE != "soft":
+    plot_module.prepare_data(solver, expr_dict)
+    plot_module.plot_6b(loss_log_obj)
+    plot_module.plot_6c7c(func_module.lambda_log)
+    plot_module.plot_6d(func_module.lambda_log)
+    plot_module.plot_6ef(func_module.lambda_log)
+
+

自定义代码请参考 完整代码 中 plotting.py 文件。

+

4. 完整代码

+

完整代码包含 PaddleScience 具体实现流程代码 holography.py,所有自定义函数代码 functions.py 和 自定义可视化代码 plotting.py。

+
holography.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This module is heavily adapted from https://github.com/lululxvi/hpinn
+"""
+
+from os import path as osp
+
+import functions as func_module
+import hydra
+import numpy as np
+import paddle
+import plotting as plot_module
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import hessian
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # open FLAG for higher order differential operator
+    paddle.framework.core.set_prim_eager_enabled(True)
+
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    model_re = ppsci.arch.MLP(**cfg.MODEL.re_net)
+    model_im = ppsci.arch.MLP(**cfg.MODEL.im_net)
+    model_eps = ppsci.arch.MLP(**cfg.MODEL.eps_net)
+
+    # initialize params
+    func_module.train_mode = cfg.TRAIN_MODE
+    loss_log_obj = []
+
+    # register transform
+    model_re.register_input_transform(func_module.transform_in)
+    model_im.register_input_transform(func_module.transform_in)
+    model_eps.register_input_transform(func_module.transform_in)
+
+    model_re.register_output_transform(func_module.transform_out_real_part)
+    model_im.register_output_transform(func_module.transform_out_imaginary_part)
+    model_eps.register_output_transform(func_module.transform_out_epsilon)
+
+    model_list = ppsci.arch.ModelList((model_re, model_im, model_eps))
+
+    # initialize Adam optimizer
+    optimizer_adam = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(
+        (model_re, model_im, model_eps)
+    )
+
+    # manually build constraint(s)
+    label_keys = ("x", "y", "bound", "e_real", "e_imaginary", "epsilon")
+    label_keys_derivative = (
+        "de_re_x",
+        "de_re_y",
+        "de_re_xx",
+        "de_re_yy",
+        "de_im_x",
+        "de_im_y",
+        "de_im_xx",
+        "de_im_yy",
+    )
+    output_expr = {
+        "x": lambda out: out["x"],
+        "y": lambda out: out["y"],
+        "bound": lambda out: out["bound"],
+        "e_real": lambda out: out["e_real"],
+        "e_imaginary": lambda out: out["e_imaginary"],
+        "epsilon": lambda out: out["epsilon"],
+        "de_re_x": lambda out: jacobian(out["e_real"], out["x"]),
+        "de_re_y": lambda out: jacobian(out["e_real"], out["y"]),
+        "de_re_xx": lambda out: hessian(out["e_real"], out["x"]),
+        "de_re_yy": lambda out: hessian(out["e_real"], out["y"]),
+        "de_im_x": lambda out: jacobian(out["e_imaginary"], out["x"]),
+        "de_im_y": lambda out: jacobian(out["e_imaginary"], out["y"]),
+        "de_im_xx": lambda out: hessian(out["e_imaginary"], out["x"]),
+        "de_im_yy": lambda out: hessian(out["e_imaginary"], out["y"]),
+    }
+
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys + label_keys_derivative,
+                "alias_dict": {
+                    "e_real": "x",
+                    "e_imaginary": "x",
+                    "epsilon": "x",
+                    **{k: "x" for k in label_keys_derivative},
+                },
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.pde_loss_fun),
+        output_expr,
+        name="sup_constraint_pde",
+    )
+    sup_constraint_obj = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys,
+                "alias_dict": {"e_real": "x", "e_imaginary": "x", "epsilon": "x"},
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.obj_loss_fun),
+        {key: lambda out, k=key: out[k] for key in label_keys},
+        name="sup_constraint_obj",
+    )
+    constraint = {
+        sup_constraint_pde.name: sup_constraint_pde,
+        sup_constraint_obj.name: sup_constraint_obj,
+    }
+
+    # manually build validator
+    sup_validator_opt = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH_VALID,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys + label_keys_derivative,
+                "alias_dict": {
+                    "x": "x_opt",
+                    "y": "y_opt",
+                    "e_real": "x_opt",
+                    "e_imaginary": "x_opt",
+                    "epsilon": "x_opt",
+                    **{k: "x_opt" for k in label_keys_derivative},
+                },
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+        output_expr,
+        {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+        name="opt_sup",
+    )
+    sup_validator_val = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH_VALID,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys + label_keys_derivative,
+                "alias_dict": {
+                    "x": "x_val",
+                    "y": "y_val",
+                    "e_real": "x_val",
+                    "e_imaginary": "x_val",
+                    "epsilon": "x_val",
+                    **{k: "x_val" for k in label_keys_derivative},
+                },
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+        output_expr,
+        {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+        name="val_sup",
+    )
+    validator = {
+        sup_validator_opt.name: sup_validator_opt,
+        sup_validator_val.name: sup_validator_val,
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        constraint,
+        cfg.output_dir,
+        optimizer_adam,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        validator=validator,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    # initialize LBFGS optimizer
+    optimizer_lbfgs = ppsci.optimizer.LBFGS(max_iter=cfg.TRAIN.max_iter)(
+        (model_re, model_im, model_eps)
+    )
+
+    # train: soft constraint, epoch=1 for lbfgs
+    if cfg.TRAIN_MODE == "soft":
+        solver = ppsci.solver.Solver(
+            model_list,
+            constraint,
+            cfg.output_dir,
+            optimizer_lbfgs,
+            None,
+            cfg.TRAIN.epochs_lbfgs,
+            cfg.TRAIN.iters_per_epoch,
+            eval_during_train=cfg.TRAIN.eval_during_train,
+            validator=validator,
+            checkpoint_path=cfg.TRAIN.checkpoint_path,
+        )
+
+        # train model
+        solver.train()
+        # evaluate after finished training
+        solver.eval()
+
+    # append objective loss for plot
+    loss_log_obj.append(func_module.loss_obj)
+
+    # penalty and augmented Lagrangian, difference between the two is updating of lambda
+    if cfg.TRAIN_MODE != "soft":
+        train_dict = ppsci.utils.reader.load_mat_file(
+            cfg.DATASET_PATH, ("x", "y", "bound")
+        )
+        in_dict = {"x": train_dict["x"], "y": train_dict["y"]}
+        expr_dict = output_expr.copy()
+        expr_dict.pop("bound")
+
+        func_module.init_lambda(in_dict, int(train_dict["bound"]))
+        func_module.lambda_log.append(
+            [
+                func_module.lambda_re.copy().squeeze(),
+                func_module.lambda_im.copy().squeeze(),
+            ]
+        )
+
+        for i in range(1, cfg.TRAIN_K + 1):
+            pred_dict = solver.predict(
+                in_dict,
+                expr_dict,
+                batch_size=np.shape(train_dict["x"])[0],
+                no_grad=False,
+            )
+            func_module.update_lambda(pred_dict, int(train_dict["bound"]))
+
+            func_module.update_mu()
+            logger.message(f"Iteration {i}: mu = {func_module.mu}\n")
+
+            solver = ppsci.solver.Solver(
+                model_list,
+                constraint,
+                cfg.output_dir,
+                optimizer_lbfgs,
+                None,
+                cfg.TRAIN.epochs_lbfgs,
+                cfg.TRAIN.iters_per_epoch,
+                eval_during_train=cfg.TRAIN.eval_during_train,
+                validator=validator,
+                checkpoint_path=cfg.TRAIN.checkpoint_path,
+            )
+
+            # train model
+            solver.train()
+            # evaluate
+            solver.eval()
+            # append objective loss for plot
+            loss_log_obj.append(func_module.loss_obj)
+
+    ################# plotting ###################
+    # log of loss
+    loss_log = np.array(func_module.loss_log).reshape(-1, 3)
+
+    plot_module.set_params(
+        cfg.TRAIN_MODE, cfg.output_dir, cfg.DATASET_PATH, cfg.DATASET_PATH_VALID
+    )
+    plot_module.plot_6a(loss_log)
+    if cfg.TRAIN_MODE != "soft":
+        plot_module.prepare_data(solver, expr_dict)
+        plot_module.plot_6b(loss_log_obj)
+        plot_module.plot_6c7c(func_module.lambda_log)
+        plot_module.plot_6d(func_module.lambda_log)
+        plot_module.plot_6ef(func_module.lambda_log)
+
+
+def evaluate(cfg: DictConfig):
+    # open FLAG for higher order differential operator
+    paddle.framework.core.set_prim_eager_enabled(True)
+
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    model_re = ppsci.arch.MLP(**cfg.MODEL.re_net)
+    model_im = ppsci.arch.MLP(**cfg.MODEL.im_net)
+    model_eps = ppsci.arch.MLP(**cfg.MODEL.eps_net)
+
+    # initialize params
+    func_module.train_mode = cfg.TRAIN_MODE
+
+    # register transform
+    model_re.register_input_transform(func_module.transform_in)
+    model_im.register_input_transform(func_module.transform_in)
+    model_eps.register_input_transform(func_module.transform_in)
+
+    model_re.register_output_transform(func_module.transform_out_real_part)
+    model_im.register_output_transform(func_module.transform_out_imaginary_part)
+    model_eps.register_output_transform(func_module.transform_out_epsilon)
+
+    model_list = ppsci.arch.ModelList((model_re, model_im, model_eps))
+
+    # manually build constraint(s)
+    label_keys = ("x", "y", "bound", "e_real", "e_imaginary", "epsilon")
+    label_keys_derivative = (
+        "de_re_x",
+        "de_re_y",
+        "de_re_xx",
+        "de_re_yy",
+        "de_im_x",
+        "de_im_y",
+        "de_im_xx",
+        "de_im_yy",
+    )
+    output_expr = {
+        "x": lambda out: out["x"],
+        "y": lambda out: out["y"],
+        "bound": lambda out: out["bound"],
+        "e_real": lambda out: out["e_real"],
+        "e_imaginary": lambda out: out["e_imaginary"],
+        "epsilon": lambda out: out["epsilon"],
+        "de_re_x": lambda out: jacobian(out["e_real"], out["x"]),
+        "de_re_y": lambda out: jacobian(out["e_real"], out["y"]),
+        "de_re_xx": lambda out: hessian(out["e_real"], out["x"]),
+        "de_re_yy": lambda out: hessian(out["e_real"], out["y"]),
+        "de_im_x": lambda out: jacobian(out["e_imaginary"], out["x"]),
+        "de_im_y": lambda out: jacobian(out["e_imaginary"], out["y"]),
+        "de_im_xx": lambda out: hessian(out["e_imaginary"], out["x"]),
+        "de_im_yy": lambda out: hessian(out["e_imaginary"], out["y"]),
+    }
+
+    # manually build validator
+    sup_validator_opt = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH_VALID,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys + label_keys_derivative,
+                "alias_dict": {
+                    "x": "x_opt",
+                    "y": "y_opt",
+                    "e_real": "x_opt",
+                    "e_imaginary": "x_opt",
+                    "epsilon": "x_opt",
+                    **{k: "x_opt" for k in label_keys_derivative},
+                },
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+        output_expr,
+        {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+        name="opt_sup",
+    )
+    sup_validator_val = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "IterableMatDataset",
+                "file_path": cfg.DATASET_PATH_VALID,
+                "input_keys": ("x", "y", "bound"),
+                "label_keys": label_keys + label_keys_derivative,
+                "alias_dict": {
+                    "x": "x_val",
+                    "y": "y_val",
+                    "e_real": "x_val",
+                    "e_imaginary": "x_val",
+                    "epsilon": "x_val",
+                    **{k: "x_val" for k in label_keys_derivative},
+                },
+            },
+        },
+        ppsci.loss.FunctionalLoss(func_module.eval_loss_fun),
+        output_expr,
+        {"mse": ppsci.metric.FunctionalMetric(func_module.eval_metric_fun)},
+        name="val_sup",
+    )
+    validator = {
+        sup_validator_opt.name: sup_validator_opt,
+        sup_validator_val.name: sup_validator_val,
+    }
+
+    solver = ppsci.solver.Solver(
+        model_list,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+
+    # evaluate
+    solver.eval()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model_re = ppsci.arch.MLP(**cfg.MODEL.re_net)
+    model_im = ppsci.arch.MLP(**cfg.MODEL.im_net)
+    model_eps = ppsci.arch.MLP(**cfg.MODEL.eps_net)
+
+    # register transform
+    model_re.register_input_transform(func_module.transform_in)
+    model_im.register_input_transform(func_module.transform_in)
+    model_eps.register_input_transform(func_module.transform_in)
+
+    model_re.register_output_transform(func_module.transform_out_real_part)
+    model_im.register_output_transform(func_module.transform_out_imaginary_part)
+    model_eps.register_output_transform(func_module.transform_out_epsilon)
+
+    # wrap to a model_list
+    model_list = ppsci.arch.ModelList((model_re, model_im, model_eps))
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model_list,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in ["x", "y"]},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    valid_dict = ppsci.utils.reader.load_mat_file(
+        cfg.DATASET_PATH_VALID, ("x_val", "y_val", "bound")
+    )
+    input_dict = {"x": valid_dict["x_val"], "y": valid_dict["y_val"]}
+
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.INFER.output_keys, output_dict.keys())
+    }
+
+    # plotting E and eps
+    N = ((func_module.l_BOX[1] - func_module.l_BOX[0]) / 0.05).astype(int)
+    input_eval = np.stack((input_dict["x"], input_dict["y"]), axis=-1).reshape(
+        N[0], N[1], 2
+    )
+    e_re = output_dict["e_re"].reshape(N[0], N[1])
+    e_im = output_dict["e_im"].reshape(N[0], N[1])
+    eps = output_dict["eps"].reshape(N[0], N[1])
+    v_visual = e_re**2 + e_im**2
+    field_visual = np.stack((v_visual, eps), axis=-1)
+    plot_module.field_name = ["Fig7_E", "Fig7_eps"]
+    plot_module.FIGNAME = "hpinns_pred"
+    plot_module.plot_field_holo(input_eval, field_visual)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="hpinns.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
functions.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This module is heavily adapted from https://github.com/lululxvi/hpinn
+"""
+
+from typing import Dict
+from typing import List
+
+import numpy as np
+import paddle
+import paddle.nn.functional as F
+
+"""All functions used in hpinns example, including functions of transform and loss."""
+
+# define constants
+BOX = np.array([[-2, -2], [2, 3]])
+DPML = 1
+OMEGA = 2 * np.pi
+SIGMA0 = -np.log(1e-20) / (4 * DPML**3 / 3)
+l_BOX = BOX + np.array([[-DPML, -DPML], [DPML, DPML]])
+beta = 2.0
+mu = 2
+
+# define variables which will be updated during training
+lambda_re: np.ndarray = None
+lambda_im: np.ndarray = None
+loss_weight: List[float] = None
+train_mode: str = None
+
+# define log variables for plotting
+loss_log = []  # record all losses, [pde, lag, obj]
+loss_obj = 0.0  # record last objective loss of each k
+lambda_log = []  # record all lambdas
+
+
+# transform
+def transform_in(input):
+    # Periodic BC in x
+    P = BOX[1][0] - BOX[0][0] + 2 * DPML
+    w = 2 * np.pi / P
+    x, y = input["x"], input["y"]
+    input_transformed = {}
+    for t in range(1, 7):
+        input_transformed[f"x_cos_{t}"] = paddle.cos(t * w * x)
+        input_transformed[f"x_sin_{t}"] = paddle.sin(t * w * x)
+    input_transformed["y"] = y
+    input_transformed["y_cos_1"] = paddle.cos(OMEGA * y)
+    input_transformed["y_sin_1"] = paddle.sin(OMEGA * y)
+
+    return input_transformed
+
+
+def transform_out_all(input, var):
+    y = input["y"]
+    # Zero Dirichlet BC
+    a, b = BOX[0][1] - DPML, BOX[1][1] + DPML
+    t = (1 - paddle.exp(a - y)) * (1 - paddle.exp(y - b))
+    return t * var
+
+
+def transform_out_real_part(input, out):
+    re = out["e_re"]
+    trans_out = transform_out_all(input, re)
+    return {"e_real": trans_out}
+
+
+def transform_out_imaginary_part(input, out):
+    im = out["e_im"]
+    trans_out = transform_out_all(input, im)
+    return {"e_imaginary": trans_out}
+
+
+def transform_out_epsilon(input, out):
+    eps = out["eps"]
+    # 1 <= eps <= 12
+    eps = F.sigmoid(eps) * 11 + 1
+    return {"epsilon": eps}
+
+
+# loss
+def init_lambda(output_dict: Dict[str, paddle.Tensor], bound: int):
+    """Init lambdas of Lagrangian and weights of losses.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+        bound (int): The bound of the data range that should be used.
+    """
+    global lambda_re, lambda_im, loss_weight
+    x, y = output_dict["x"], output_dict["y"]
+    lambda_re = np.zeros((len(x[bound:]), 1), paddle.get_default_dtype())
+    lambda_im = np.zeros((len(y[bound:]), 1), paddle.get_default_dtype())
+    # loss_weight: [PDE loss 1, PDE loss 2, Lagrangian loss 1, Lagrangian loss 2, objective loss]
+    if train_mode == "aug_lag":
+        loss_weight = [0.5 * mu] * 2 + [1.0, 1.0] + [1.0]
+    else:
+        loss_weight = [0.5 * mu] * 2 + [0.0, 0.0] + [1.0]
+
+
+def update_lambda(output_dict: Dict[str, paddle.Tensor], bound: int):
+    """Update lambdas of Lagrangian.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+        bound (int): The bound of the data range that should be used.
+    """
+    global lambda_re, lambda_im, lambda_log
+    loss_re, loss_im = compute_real_and_imaginary_loss(output_dict)
+    loss_re = loss_re[bound:]
+    loss_im = loss_im[bound:]
+    lambda_re += mu * loss_re.numpy()
+    lambda_im += mu * loss_im.numpy()
+    lambda_log.append([lambda_re.copy().squeeze(), lambda_im.copy().squeeze()])
+
+
+def update_mu():
+    """Update mu."""
+    global mu, loss_weight
+    mu *= beta
+    loss_weight[:2] = [0.5 * mu] * 2
+
+
+def _sigma_1(d):
+    return SIGMA0 * d**2 * np.heaviside(d, 0)
+
+
+def _sigma_2(d):
+    return 2 * SIGMA0 * d * np.heaviside(d, 0)
+
+
+def sigma(x, a, b):
+    """sigma(x) = 0 if a < x < b, else grows cubically from zero."""
+    return _sigma_1(a - x) + _sigma_1(x - b)
+
+
+def dsigma(x, a, b):
+    return -_sigma_2(a - x) + _sigma_2(x - b)
+
+
+def perfectly_matched_layers(x: paddle.Tensor, y: paddle.Tensor):
+    """Apply the technique of perfectly matched layers(PMLs) proposed by paper arXiv:2108.05348.
+
+    Args:
+        x (paddle.Tensor): one of input contains tensor.
+        y (paddle.Tensor): one of input contains tensor.
+
+    Returns:
+        np.ndarray: Parameters of pde formula.
+    """
+    x = x.numpy()
+    y = y.numpy()
+
+    sigma_x = sigma(x, BOX[0][0], BOX[1][0])
+    AB1 = 1 / (1 + 1j / OMEGA * sigma_x) ** 2
+    A1, B1 = AB1.real, AB1.imag
+
+    dsigma_x = dsigma(x, BOX[0][0], BOX[1][0])
+    AB2 = -1j / OMEGA * dsigma_x * AB1 / (1 + 1j / OMEGA * sigma_x)
+    A2, B2 = AB2.real, AB2.imag
+
+    sigma_y = sigma(y, BOX[0][1], BOX[1][1])
+    AB3 = 1 / (1 + 1j / OMEGA * sigma_y) ** 2
+    A3, B3 = AB3.real, AB3.imag
+
+    dsigma_y = dsigma(y, BOX[0][1], BOX[1][1])
+    AB4 = -1j / OMEGA * dsigma_y * AB3 / (1 + 1j / OMEGA * sigma_y)
+    A4, B4 = AB4.real, AB4.imag
+    return A1, B1, A2, B2, A3, B3, A4, B4
+
+
+def obj_func_J(y):
+    # Approximate the objective function
+    y = y.numpy() + 1.5
+    h = 0.2
+    return 1 / (h * np.pi**0.5) * np.exp(-((y / h) ** 2)) * (np.abs(y) < 0.5)
+
+
+def compute_real_and_imaginary_loss(
+    output_dict: Dict[str, paddle.Tensor]
+) -> paddle.Tensor:
+    """Compute real and imaginary_loss.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: Real and imaginary_loss.
+    """
+    x, y = output_dict["x"], output_dict["y"]
+    e_re = output_dict["e_real"]
+    e_im = output_dict["e_imaginary"]
+    eps = output_dict["epsilon"]
+
+    condition = np.logical_and(y.numpy() < 0, y.numpy() > -1).astype(
+        paddle.get_default_dtype()
+    )
+
+    eps = eps * condition + 1 - condition
+
+    de_re_x = output_dict["de_re_x"]
+    de_re_y = output_dict["de_re_y"]
+    de_re_xx = output_dict["de_re_xx"]
+    de_re_yy = output_dict["de_re_yy"]
+    de_im_x = output_dict["de_im_x"]
+    de_im_y = output_dict["de_im_y"]
+    de_im_xx = output_dict["de_im_xx"]
+    de_im_yy = output_dict["de_im_yy"]
+
+    a1, b1, a2, b2, a3, b3, a4, b4 = perfectly_matched_layers(x, y)
+
+    loss_re = (
+        (a1 * de_re_xx + a2 * de_re_x + a3 * de_re_yy + a4 * de_re_y) / OMEGA
+        - (b1 * de_im_xx + b2 * de_im_x + b3 * de_im_yy + b4 * de_im_y) / OMEGA
+        + eps * OMEGA * e_re
+    )
+    loss_im = (
+        (a1 * de_im_xx + a2 * de_im_x + a3 * de_im_yy + a4 * de_im_y) / OMEGA
+        + (b1 * de_re_xx + b2 * de_re_x + b3 * de_re_yy + b4 * de_re_y) / OMEGA
+        + eps * OMEGA * e_im
+        + obj_func_J(y)
+    )
+    return loss_re, loss_im
+
+
+def pde_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute pde loss and lagrangian loss.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: PDE loss (and lagrangian loss if using Augmented Lagrangian method).
+    """
+    global loss_log
+    bound = int(output_dict["bound"])
+    loss_re, loss_im = compute_real_and_imaginary_loss(output_dict)
+    loss_re = loss_re[bound:]
+    loss_im = loss_im[bound:]
+
+    loss_eqs1 = paddle.mean(loss_re**2)
+    loss_eqs2 = paddle.mean(loss_im**2)
+    # augmented_Lagrangian
+    if lambda_im is None:
+        init_lambda(output_dict, bound)
+    loss_lag1 = paddle.mean(loss_re * lambda_re)
+    loss_lag2 = paddle.mean(loss_im * lambda_im)
+
+    losses = (
+        loss_weight[0] * loss_eqs1
+        + loss_weight[1] * loss_eqs2
+        + loss_weight[2] * loss_lag1
+        + loss_weight[3] * loss_lag2
+    )
+    loss_log.append(float(loss_eqs1 + loss_eqs2))  # for plotting
+    loss_log.append(float(loss_lag1 + loss_lag2))  # for plotting
+    return {"pde": losses}
+
+
+def obj_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute objective loss.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: Objective loss.
+    """
+    global loss_log, loss_obj
+    x, y = output_dict["x"], output_dict["y"]
+    bound = int(output_dict["bound"])
+    e_re = output_dict["e_real"]
+    e_im = output_dict["e_imaginary"]
+
+    f1 = paddle.heaviside((x + 0.5) * (0.5 - x), paddle.to_tensor(0.5))
+    f2 = paddle.heaviside((y - 1) * (2 - y), paddle.to_tensor(0.5))
+    j = e_re[:bound] ** 2 + e_im[:bound] ** 2 - f1[:bound] * f2[:bound]
+    loss_opt_area = paddle.mean(j**2)
+
+    if lambda_im is None:
+        init_lambda(output_dict, bound)
+    losses = loss_weight[4] * loss_opt_area
+    loss_log.append(float(loss_opt_area))  # for plotting
+    loss_obj = float(loss_opt_area)  # for plotting
+    return {"obj": losses}
+
+
+def eval_loss_fun(output_dict: Dict[str, paddle.Tensor], *args) -> paddle.Tensor:
+    """Compute objective loss for evaluation.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        paddle.Tensor: Objective loss.
+    """
+    x, y = output_dict["x"], output_dict["y"]
+    e_re = output_dict["e_real"]
+    e_im = output_dict["e_imaginary"]
+
+    f1 = paddle.heaviside((x + 0.5) * (0.5 - x), paddle.to_tensor(0.5))
+    f2 = paddle.heaviside((y - 1) * (2 - y), paddle.to_tensor(0.5))
+    j = e_re**2 + e_im**2 - f1 * f2
+    losses = paddle.mean(j**2)
+
+    return {"eval": losses}
+
+
+def eval_metric_fun(
+    output_dict: Dict[str, paddle.Tensor], *args
+) -> Dict[str, paddle.Tensor]:
+    """Compute metric for evaluation.
+
+    Args:
+        output_dict (Dict[str, paddle.Tensor]): Dict of outputs contains tensor.
+
+    Returns:
+        Dict[str, paddle.Tensor]: MSE metric.
+    """
+    loss_re, loss_im = compute_real_and_imaginary_loss(output_dict)
+    eps_opt = paddle.concat([loss_re, loss_im], axis=-1)
+    metric = paddle.mean(eps_opt**2)
+
+    metric_dict = {"eval_metric": metric}
+    return metric_dict
+
+
plotting.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This module is heavily adapted from https://github.com/lululxvi/hpinn
+"""
+
+import os
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Optional
+
+import functions as func_module
+import matplotlib.pyplot as plt
+import numpy as np
+import seaborn as sns
+from matplotlib import ticker
+
+import ppsci
+
+"""All plotting functions."""
+
+# define constants
+font = {"weight": "normal", "size": 10}
+input_name = ("x", "y")
+field_name = [
+    "Fig7_E",
+    "Fig7_eps",
+    "Fig_6C_lambda_re_1",
+    "Fig_6C_lambda_im_1",
+    "Fig_6C_lambda_re_4",
+    "Fig_6C_lambda_im_4",
+    "Fig_6C_lambda_re_9",
+    "Fig_6C_lambda_im_9",
+]
+
+# define constants which will be assigned later
+FIGNAME: str = ""
+OUTPUT_DIR: str = ""
+DATASET_PATH: str = ""
+DATASET_PATH_VALID: str = ""
+input_valid: np.ndarray = None
+output_valid: np.ndarray = None
+input_train: np.ndarray = None
+
+
+def set_params(figname, output_dir, dataset_path, dataset_path_valid):
+    global FIGNAME, OUTPUT_DIR, DATASET_PATH, DATASET_PATH_VALID
+    FIGNAME = figname
+    OUTPUT_DIR = output_dir + "figure/"
+    os.makedirs(OUTPUT_DIR, exist_ok=True)
+    DATASET_PATH = dataset_path
+    DATASET_PATH_VALID = dataset_path_valid
+
+
+def prepare_data(solver: ppsci.solver.Solver, expr_dict: Dict[str, Callable]):
+    """Prepare data of input of training and validation and generate
+        output of validation by predicting.
+
+    Args:
+        solver (ppsci.solver.Solver): Object of ppsci.solver.Solver().
+        expr_dict (Dict[str, Callable]): Expression dict, which guide to
+            compute equation variable with callable function.
+    """
+    global input_valid, output_valid, input_train
+    # train data
+    train_dict = ppsci.utils.reader.load_mat_file(DATASET_PATH, ("x", "y", "bound"))
+
+    bound = int(train_dict["bound"])
+    x_train = train_dict["x"][bound:]
+    y_train = train_dict["y"][bound:]
+    input_train = np.stack((x_train, y_train), axis=-1).reshape(-1, 2)
+
+    # valid data
+    N = ((func_module.l_BOX[1] - func_module.l_BOX[0]) / 0.05).astype(int)
+
+    valid_dict = ppsci.utils.reader.load_mat_file(
+        DATASET_PATH_VALID, ("x_val", "y_val", "bound")
+    )
+    in_dict_val = {"x": valid_dict["x_val"], "y": valid_dict["y_val"]}
+    func_module.init_lambda(in_dict_val, int(valid_dict["bound"]))
+
+    pred_dict_val = solver.predict(
+        in_dict_val,
+        expr_dict,
+        batch_size=np.shape(valid_dict["x_val"])[0],
+        no_grad=False,
+        return_numpy=True,
+    )
+
+    input_valid = np.stack((valid_dict["x_val"], valid_dict["y_val"]), axis=-1).reshape(
+        N[0], N[1], 2
+    )
+    output_valid = np.array(
+        [
+            pred_dict_val["e_real"],
+            pred_dict_val["e_imaginary"],
+            pred_dict_val["epsilon"],
+        ]
+    ).T.reshape(N[0], N[1], 3)
+
+
+def plot_field_holo(
+    coord_visual: np.ndarray,
+    field_visual: np.ndarray,
+    coord_lambda: Optional[np.ndarray] = None,
+    field_lambda: Optional[np.ndarray] = None,
+):
+    """Plot fields of of holography example.
+
+    Args:
+        coord_visual (np.ndarray): The coord of epsilon and |E|**2.
+        field_visual (np.ndarray): The filed of epsilon and |E|**2.
+        coord_lambda (Optional[np.ndarray], optional): The coord of lambda. Defaults to None.
+        field_lambda (Optional[np.ndarray], optional): The filed of lambda. Defaults to None.
+    """
+    fmin, fmax = np.array([0, 1.0]), np.array([0.6, 12])
+    cmin, cmax = coord_visual.min(axis=(0, 1)), coord_visual.max(axis=(0, 1))
+    emin, emax = np.array([-3, -1]), np.array([3, 0])
+    x_pos = coord_visual[:, :, 0]
+    y_pos = coord_visual[:, :, 1]
+
+    for fi in range(len(field_name)):
+        if fi == 0:
+            # Fig7_E
+            plt.figure(101, figsize=(8, 6))
+            plt.clf()
+            plt.rcParams["font.size"] = 20
+            f_true = field_visual[..., fi]
+            plt.pcolormesh(
+                x_pos,
+                y_pos,
+                f_true,
+                cmap="rainbow",
+                shading="gouraud",
+                antialiased=True,
+                snap=True,
+            )
+            cb = plt.colorbar()
+            plt.axis((cmin[0], cmax[0], cmin[1], cmax[1]))
+            plt.clim(vmin=fmin[fi], vmax=fmax[fi])
+        elif fi == 1:
+            # Fig7_eps
+            plt.figure(201, figsize=(8, 1.5))
+            plt.clf()
+            plt.rcParams["font.size"] = 20
+            f_true = field_visual[..., fi]
+            plt.pcolormesh(
+                x_pos,
+                y_pos,
+                f_true,
+                cmap="rainbow",
+                shading="gouraud",
+                antialiased=True,
+                snap=True,
+            )
+            cb = plt.colorbar()
+            plt.axis((emin[0], emax[0], emin[1], emax[1]))
+            plt.clim(vmin=fmin[fi], vmax=fmax[fi])
+        elif coord_lambda is not None and field_lambda is not None:
+            # Fig_6C_lambda_
+            plt.figure(fi * 100 + 101, figsize=(8, 6))
+            plt.clf()
+            plt.rcParams["font.size"] = 20
+            f_true = field_lambda[..., fi - 2]
+            plt.scatter(
+                coord_lambda[..., 0],
+                coord_lambda[..., 1],
+                c=f_true,
+                cmap="rainbow",
+                alpha=0.6,
+            )
+            cb = plt.colorbar()
+            plt.axis((cmin[0], cmax[0], cmin[1], cmax[1]))
+
+        # colorbar settings
+        cb.ax.tick_params(labelsize=20)
+        tick_locator = ticker.MaxNLocator(
+            nbins=5
+        )  # the number of scale values ​​on the colorbar
+        cb.locator = tick_locator
+        cb.update_ticks()
+
+        plt.xlabel(f"${str(input_name[0])}$", fontdict=font)
+        plt.ylabel(f"${str(input_name[1])}$", fontdict=font)
+        plt.yticks(size=10)
+        plt.xticks(size=10)
+        plt.savefig(
+            os.path.join(
+                OUTPUT_DIR,
+                f"{FIGNAME}_{str(field_name[fi])}.jpg",
+            )
+        )
+
+
+def plot_6a(log_loss: np.ndarray):
+    """Plot Fig.6 A of paper.
+
+    Args:
+        log_loss (np.ndarray): Losses of all training's iterations.
+    """
+    plt.figure(300, figsize=(8, 6))
+    smooth_step = 100  # how many steps of loss are squeezed to one point, num_points is epoch/smooth_step
+    if log_loss.shape[0] % smooth_step != 0:
+        vis_loss_ = log_loss[: -(log_loss.shape[0] % smooth_step), :].reshape(
+            -1, smooth_step, log_loss.shape[1]
+        )
+    else:
+        vis_loss_ = log_loss.reshape(-1, smooth_step, log_loss.shape[1])
+
+    vis_loss = vis_loss_.mean(axis=1).reshape(-1, 3)
+    vis_loss_total = vis_loss[:, :].sum(axis=1)
+    vis_loss[:, 1] = vis_loss[:, 2]
+    vis_loss[:, 2] = vis_loss_total
+    for i in range(vis_loss.shape[1]):
+        plt.semilogy(np.arange(vis_loss.shape[0]) * smooth_step, vis_loss[:, i])
+    plt.legend(
+        ["PDE loss", "Objective loss", "Total loss"],
+        loc="lower left",
+        prop=font,
+    )
+    plt.xlabel("Iteration ", fontdict=font)
+    plt.ylabel("Loss ", fontdict=font)
+    plt.grid()
+    plt.yticks(size=10)
+    plt.xticks(size=10)
+    plt.savefig(os.path.join(OUTPUT_DIR, f"{FIGNAME}_Fig6_A.jpg"))
+
+
+def plot_6b(log_loss_obj: List[float]):
+    """Plot Fig.6 B of paper.
+
+    Args:
+        log_loss_obj (List[float]): Objective losses of last iteration of each k.
+    """
+    plt.figure(400, figsize=(10, 6))
+    plt.clf()
+    plt.plot(np.arange(len(log_loss_obj)), log_loss_obj, "bo-")
+    plt.xlabel("k", fontdict=font)
+    plt.ylabel("Objective", fontdict=font)
+    plt.grid()
+    plt.yticks(size=10)
+    plt.xticks(size=10)
+    plt.savefig(os.path.join(OUTPUT_DIR, f"{FIGNAME}_Fig6_B.jpg"))
+
+
+def plot_6c7c(log_lambda: List[np.ndarray]):
+    """Plot Fig.6 Cs and Fig.7.Cs of paper.
+
+    Args:
+        log_lambda (List[np.ndarray]): Lambdas of each k.
+    """
+    # plot Fig.6 Cs and Fig.7.Cs of paper
+    global input_valid, output_valid, input_train
+
+    field_lambda = np.concatenate(
+        [log_lambda[1], log_lambda[4], log_lambda[9]], axis=0
+    ).T
+    v_visual = output_valid[..., 0] ** 2 + output_valid[..., 1] ** 2
+    field_visual = np.stack((v_visual, output_valid[..., -1]), axis=-1)
+    plot_field_holo(input_valid, field_visual, input_train, field_lambda)
+
+
+def plot_6d(log_lambda: List[np.ndarray]):
+    """Plot Fig.6 D of paper.
+
+    Args:
+        log_lambda (List[np.ndarray]): Lambdas of each k.
+    """
+    # lambda/mu
+    mu_ = 2 ** np.arange(1, 11)
+    log_lambda = np.array(log_lambda) / mu_[:, None, None]
+    # randomly pick 3 lambda points to represent all points of each k
+    ind = np.random.randint(low=0, high=np.shape(log_lambda)[-1], size=3)
+    la_mu_ind = log_lambda[:, :, ind]
+    marker = ["ro-", "bo:", "r*-", "b*:", "rp-", "bp:"]
+    plt.figure(500, figsize=(7, 5))
+    plt.clf()
+    for i in range(6):
+        plt.plot(
+            np.arange(0, 10),
+            la_mu_ind[:, int(i % 2), int(i / 2)],
+            marker[i],
+            linewidth=2,
+        )
+    plt.legend(
+        ["Re, 1", "Im, 1", "Re, 2", "Im, 2", "Re, 3", "Im, 3"],
+        loc="upper right",
+        prop=font,
+    )
+    plt.grid()
+    plt.xlabel("k", fontdict=font)
+    plt.ylabel(r"$ \lambda^k / \mu^k_F$", fontdict=font)
+    plt.yticks(size=12)
+    plt.xticks(size=12)
+    plt.savefig(os.path.join(OUTPUT_DIR, f"{FIGNAME}_Fig6_D_lambda.jpg"))
+
+
+def plot_6ef(log_lambda: List[np.ndarray]):
+    """Plot Fig.6 E and Fig.6.F of paper.
+
+    Args:
+        log_lambda (List[np.ndarray]): Lambdas of each k.
+    """
+    # lambda/mu
+    mu_ = 2 ** np.arange(1, 11)
+    log_lambda = np.array(log_lambda) / mu_[:, None, None]
+    # pick k=1,4,6,9
+    iter_ind = [1, 4, 6, 9]
+    plt.figure(600, figsize=(5, 5))
+    plt.clf()
+    for i in iter_ind:
+        sns.kdeplot(log_lambda[i, 0, :], label="k = " + str(i), cut=0, linewidth=2)
+    plt.legend(prop=font)
+    plt.grid()
+    plt.xlim([-0.1, 0.1])
+    plt.xlabel(r"$ \lambda^k_{Re} / \mu^k_F$", fontdict=font)
+    plt.ylabel("Frequency", fontdict=font)
+    plt.yticks(size=12)
+    plt.xticks(size=12)
+    plt.savefig(os.path.join(OUTPUT_DIR, f"{FIGNAME}_Fig6_E.jpg"))
+
+    plt.figure(700, figsize=(5, 5))
+    plt.clf()
+    for i in iter_ind:
+        sns.kdeplot(log_lambda[i, 1, :], label="k = " + str(i), cut=0, linewidth=2)
+    plt.legend(prop=font)
+    plt.grid()
+    plt.xlim([-0.1, 0.1])
+    plt.xlabel(r"$ \lambda^k_{Im} / \mu^k_F$", fontdict=font)
+    plt.ylabel("Frequency", fontdict=font)
+    plt.yticks(size=12)
+    plt.xticks(size=12)
+    plt.savefig(os.path.join(OUTPUT_DIR, f"{FIGNAME}_Fig6_F.jpg"))
+
+

5. 结果展示

+

参考 问题定义,下图展示了训练过程中 loss 变化、参数 lambda 和参数 mu 与增强的拉格朗日方法中训练论次 k 的变化、电场 E 和介电常数 epsilon 最终预测的值。

+

下图展示了对于一个定义的方形域内,电磁波传播的情况的预测。预测结果与有限差分频域(FDFD)方法的结果基本一致。

+

训练过程中的 loss 值变化:

+
+

holograpy_result_6A +

+
训练过程 loss 值随 iteration 变化
+
+

objective loss 值随训练轮次 k 的变化:

+
+

holograpy_result_6B +

+
k 值对应 objective loss 值
+
+

k=1,4,9 时对应参数 lambda 实部和虚部的值:

+
+

holograpy_result_6C +

+
k=1,4,9 时对应 lambda 值
+
+

参数 lambda 和参数 mu 的比值随训练轮次 k 的变化:

+
+

holograpy_result_6D +

+
k 值对应 lambda/mu 值
+
+

参数 lambda 和参数 mu 实部的比值随训练轮次 k=1,4,6,9 时出现的频率,曲线越“尖”说明值越趋于统一,收敛的越好:

+
+

holograpy_result_6E +

+
k=1,4,6,9 时对应实部 lambda/mu 值出现频率
+
+

参数 lambda 和参数 mu 虚部的比值随训练轮次 k=1,4,6,9 时出现的频率,曲线越“尖”说明值越趋于统一,收敛的越好:

+
+

holograpy_result_6F +

+
k=1,4,6,9 时对应虚部 lambda/mu 值出现频率
+
+

电场 E 值:

+
+

holograpy_result_7C +

+
E 值
+
+

介电常数 epsilon 值:

+
+

holograpy_result_7eps +

+
epsilon 值
+
+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/labelfree_DNN_surrogate/index.html b/zh/examples/labelfree_DNN_surrogate/index.html new file mode 100644 index 0000000000..dbade46e9b --- /dev/null +++ b/zh/examples/labelfree_DNN_surrogate/index.html @@ -0,0 +1,6659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Labelfree_DNN_surrogate - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

LabelFree-DNN-Surrogate (Aneurysm flow & Pipe flow)

+
+
+
+

案例一:Pipe Flow

+
python poiseuille_flow.py
+
+

案例二:Aneurysm Flow

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/LabelFree-DNN-Surrogate/LabelFree-DNN-Surrogate_data.zip
+unzip LabelFree-DNN-Surrogate_data.zip
+
+python aneurysm_flow.py
+
+
+
+

案例一:Pipe Flow

+
python poiseuille_flow.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/LabelFree-DNN-Surrogate/poiseuille_flow_pretrained.pdparams
+
+

案例二:Aneurysm Flow

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/LabelFree-DNN-Surrogate/LabelFree-DNN-Surrogate_data.zip
+unzip LabelFree-DNN-Surrogate_data.zip
+
+python aneurysm_flow.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/LabelFree-DNN-Surrogate/aneurysm_flow.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
aneurysm_flow.pdparamsL-2 error u : 2.548e-4
L-2 error v : 7.169e-5
+

1. 背景简介

+

流体动力学问题的数值模拟主要依赖于使用多项式将控制方程在空间或/和时间上离散化为有限维代数系统。由于物理的多尺度特性和对复杂几何体进行网格划分的敏感性,这样的过程对于大多数实时应用程序(例如,临床诊断和手术计划)和多查询分析(例如,优化设计和不确定性量化)。在本文中,我们提供了一种物理约束的 DL 方法,用于在不依赖任何模拟数据的情况下对流体流动进行代理建模。 具体来说,设计了一种结构化深度神经网络 (DNN) 架构来强制执行初始条件和边界条件,并将控制偏微分方程(即 Navier-Stokes 方程)纳入 DNN的损失中以驱动训练。 对与血液动力学应用相关的许多内部流动进行了数值实验,并研究了流体特性和域几何中不确定性的前向传播。结果表明,DL 代理近似与第一原理数值模拟之间的流场和前向传播不确定性非常吻合。

+

2. 案例一:PipeFlow

+

2.1 问题定义

+

管道流体是一类非常常见和常用的流体系统,例如动脉中的血液或气管中的气流,一般管道流受到管道两端的压力差驱动,或者重力体积力驱动。 +在心血管系统中,前者更占主导地位,因为血流主要受心脏泵送引起的压降控制。 一般来说,模拟管中的流体动力学需要用数值方法求解完整的 Navier-Stokes 方程,但如果管是直的并且具有恒定的圆形横截面,则可以获得完全发展的稳态流动的解析解,即 一个理想的基准来验证所提出方法的性能。 因此,我们首先研究二维圆管中的流动(也称为泊肃叶流)。

+

质量守恒:

+
\[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} + \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} + \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) +\]
+

我们只关注这种完全发展的流动并且在边界施加了无滑移边界条件。与传统PINNs方法不同的是,我们将无滑动边界条件通过速度函数假设的方式强制施加在边界上: +对于流体域边界和流体域内部圆周边界,则需施加 Dirichlet 边界条件:

+
+

pipe +

+
流场示意图
+
+

流体域入口边界:

+
\[ +p=0.1 +\]
+

流体域出口边界:

+
\[ +p=0 +\]
+

流体域上下边界:

+
\[ +u=0, v=0 +\]
+

2.2 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

2.2.1 模型构建

+

在本案例中,每一个已知的坐标点和该点的动力粘性系数三元组 \((x, y, \nu)\) 都有自身的横向速度 \(u\)、纵向速度 \(v\)、压力 \(p\) +三个待求解的未知量,我们在这里使用比较简单的三个 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y, \nu)\)\((u, v, p)\) 的映射函数 \(f_1, f_2, f_3: \mathbb{R}^3 \to \mathbb{R}^3\) ,即:

+
\[ +u= transform_{output}(f_1(transform_{input}(x, y, \nu))) +\]
+
\[ +v= transform_{output}(f_2(transform_{input}(x, y, \nu))) +\]
+
\[ +p= transform_{output}(f_3(transform_{input}(x, y, \nu))) +\]
+

上式中 \(f_1, f_2, f_3\) 即为 MLP 模型本身,\(transform_{input}, transform_{output}\), 表示施加额外的结构化自定义层,用于施加约束和丰富输入,用 PaddleScience 代码表示如下:

+
# set model
+model_u = ppsci.arch.MLP(**cfg.MODEL.u_net)
+model_v = ppsci.arch.MLP(**cfg.MODEL.v_net)
+model_p = ppsci.arch.MLP(**cfg.MODEL.p_net)
+
+def input_trans(input):
+    x, y = input["x"], input["y"]
+    nu = input["nu"]
+    b = 2 * np.pi / (X_OUT - cfg.X_IN)
+    c = np.pi * (cfg.X_IN + X_OUT) / (cfg.X_IN - X_OUT)
+    sin_x = cfg.X_IN * paddle.sin(b * x + c)
+    cos_x = cfg.X_IN * paddle.cos(b * x + c)
+    return {"sin(x)": sin_x, "cos(x)": cos_x, "x": x, "y": y, "nu": nu}
+
+def output_trans_u(input, out):
+    return {"u": out["u"] * (cfg.R**2 - input["y"] ** 2)}
+
+def output_trans_v(input, out):
+    return {"v": (cfg.R**2 - input["y"] ** 2) * out["v"]}
+
+def output_trans_p(input, out):
+    return {
+        "p": (
+            (cfg.P_IN - cfg.P_OUT) * (X_OUT - input["x"]) / cfg.L
+            + (cfg.X_IN - input["x"]) * (X_OUT - input["x"]) * out["p"]
+        )
+    }
+
+model_u.register_input_transform(input_trans)
+model_v.register_input_transform(input_trans)
+model_p.register_input_transform(input_trans)
+model_u.register_output_transform(output_trans_u)
+model_v.register_output_transform(output_trans_v)
+model_p.register_output_transform(output_trans_p)
+model = ppsci.arch.ModelList((model_u, model_v, model_p))
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ["x"、 "y"、 "nu"],输出变量名是 ["u"、 "v"、 "p"],这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了三个拥有 3 层隐藏神经元和 1 层输出层神经元的神经网络,每层神经元数为 50,使用 "swish" 作为激活函数的神经网络模型 model_u model_v model_p

+

2.2.2 方程构建

+

由于本案例使用的是 Navier-Stokes 方程的2维稳态形式,因此可以直接使用 PaddleScience 内置的 NavierStokes

+
# set euqation
+equation = {
+    "NavierStokes": ppsci.equation.NavierStokes(
+        nu="nu", rho=cfg.RHO, dim=2, time=False
+    )
+}
+
+

在实例化 NavierStokes 类时需指定必要的参数:动力粘度 \(\nu\) 为网络输出, 流体密度 \(\rho=1.0\)

+

2.2.3 计算域构建

+

本文中本案例的计算域和参数自变量 \(\nu\)numpy随机数生成的点云构成,因此可以直接使用 PaddleScience 内置的点云几何 PointCloud 组合成空间的 Geometry 计算域。

+
45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
## prepare data with (?, 2)
+data_1d_x = np.linspace(
+    cfg.X_IN, X_OUT, cfg.N_x, endpoint=True, dtype=paddle.get_default_dtype()
+)
+data_1d_y = np.linspace(
+    Y_START, Y_END, cfg.N_y, endpoint=True, dtype=paddle.get_default_dtype()
+)
+data_1d_nu = np.linspace(
+    NU_START, NU_END, cfg.N_p, endpoint=True, dtype=paddle.get_default_dtype()
+)
+
+data_2d_xy = (
+    np.array(np.meshgrid(data_1d_x, data_1d_y, data_1d_nu)).reshape(3, -1).T
+)
+data_2d_xy_shuffle = copy.deepcopy(data_2d_xy)
+np.random.shuffle(data_2d_xy_shuffle)
+
+input_x = data_2d_xy_shuffle[:, 0].reshape(data_2d_xy_shuffle.shape[0], 1)
+input_y = data_2d_xy_shuffle[:, 1].reshape(data_2d_xy_shuffle.shape[0], 1)
+input_nu = data_2d_xy_shuffle[:, 2].reshape(data_2d_xy_shuffle.shape[0], 1)
+
+interior_geom = ppsci.geometry.PointCloud(
+    interior={"x": input_x, "y": input_y, "nu": input_nu},
+    coord_keys=("x", "y", "nu"),
+)
+
+

2.2.4 约束构建

+

根据 2.1 问题定义 得到的公式和和边界条件,对应了在计算域中指导模型训练的几个约束条件,即:

+
    +
  • +

    施加在流体域内部点上的Navier-Stokes 方程约束

    +

    质量守恒:

    +
    \[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
    +

    \(x\) 动量守恒:

    +
    \[ +u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} +\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} - \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) = 0 +\]
    +

    \(y\) 动量守恒:

    +
    \[ +u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} +\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} - \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) = 0 +\]
    +

    为了方便获取中间变量,NavierStokes 类内部将上式左侧的结果分别命名为 continuity, momentum_x, momentum_y

    +
  • +
  • +

    施加在流体域入出口、流体域上下血管壁边界的的 Dirichlet 边界条件约束。作为本文创新点之一,此案例创新性的使用了结构化边界条件,即通过网络的输出层后面,增加一层公式层,来施加边界条件(公式在边界处值为零)。避免了数据点作为边界条件无法有效约束的不足。统一使用用类函数Transform()进行初始化和管理。具体的推理过程为:

    +

    流体域上下边界(血管壁)修正函数的公式形式为:

    +
    \[ +\hat{u}(t,x,\theta;W,b) = u_{par}(t,x,\theta) + D(t,x,\theta)\tilde{u}(t,x,\theta;W,b) +\]
    +
    \[ +\hat{p}(t,x,\theta;W,b) = p_{par}(t,x,\theta) + D(t,x,\theta)\tilde{p}(t,x,\theta;W,b) +\]
    +

    其中\(u_{par}\)\(p_{par}\)是满足边界条件和初始条件的特解,具体的修正函数带入后得到:

    +
    \[ +\hat{u} = (\dfrac{d^2}{4} - y^2) \tilde{u} +\]
    +
    \[ +\hat{v} = (\dfrac{d^2}{4} - y^2) \tilde{v} +\]
    +
    \[ +\hat{p} = \dfrac{x - x_{in}}{x_{out} - x_{in}}p_{out} + \dfrac{x_{out} - x}{x_{out} - x_{in}}p_{in} + (x - x_{in})(x_{out} - x) \tilde{p} +\]
    +
  • +
+

接下来使用 PaddleScience 内置的 InteriorConstraint 和模型Transform自定义层,构建上述两种约束条件。

+
    +
  • +

    内部点约束

    +

    以作用在流体域内部点上的 InteriorConstraint 为例,代码如下:

    +
    pde_constraint = ppsci.constraint.InteriorConstraint(
    +    equation["NavierStokes"].equations,
    +    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
    +    geom=interior_geom,
    +    dataloader_cfg={
    +        "dataset": "NamedArrayDataset",
    +        "num_workers": 1,
    +        "batch_size": cfg.TRAIN.batch_size.pde_constraint,
    +        "iters_per_epoch": ITERS_PER_EPOCH,
    +        "sampler": {
    +            "name": "BatchSampler",
    +            "shuffle": False,
    +            "drop_last": False,
    +        },
    +    },
    +    loss=ppsci.loss.MSELoss("mean"),
    +    evenly=True,
    +    name="EQ",
    +)
    +# wrap constraints together
    +constraint = {pde_constraint.name: pde_constraint}
    +
    +

    InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 2.2.2 方程构建 章节中实例化好的 equation["NavierStokes"].equations

    +

    第二个参数是约束变量的目标值,在本问题中我们希望 Navier-Stokes 方程产生的三个中间结果 continuity, momentum_x, momentum_y 被优化至 0,因此将它们的目标值全部设为 0;

    +

    第三个参数是约束方程作用的计算域,此处填入在 2.2.3 计算域构建 章节实例化好的 interior_geom 即可;

    +

    第四个参数是在计算域上的采样配置,此处我们使用分批次数据点训练,因此 dataset 字段设置为 NamedArrayDatasetiters_per_epoch 也设置为 1,采样点数 batch_size 设为 128;

    +

    第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

    +

    第六个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

    +
  • +
+

2.2.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,使用3000轮训练轮数,学习率设为 0.005。

+

2.2.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

2.2.7 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    save_freq=cfg.TRAIN.save_freq,
+    equation=equation,
+)
+solver.train()
+
+

另一方面,此案例的可视化和定量评估主要依赖于:

+
    +
  1. +

    \(x=0\) 截面速度 \(u(y)\)\(y\) 在四种不同的动力粘性系数 \({\nu}\) 采样下的曲线和解析解的对比

    +
  2. +
  3. +

    当我们选取截断高斯分布的动力粘性系数 \({\nu}\) 采样(均值为 \(\hat{\nu} = 10^{−3}\), 方差 \(\sigma_{\nu}​=2.67 \times 10^{−4}\)),中心处速度的概率密度函数和解析解对比

    +
  4. +
+
def evaluate(cfg: DictConfig):
+    NU_MEAN = 0.001
+    NU_STD = 0.9
+    L = 1.0  # length of pipe
+    R = 0.05  # radius of pipe
+    RHO = 1  # density
+    P_OUT = 0  # pressure at the outlet of pipe
+    P_IN = 0.1  # pressure at the inlet of pipe
+    N_x = 10
+    N_y = 50
+    N_p = 50
+    X_IN = 0
+    X_OUT = X_IN + L
+    Y_START = -R
+    Y_END = Y_START + 2 * R
+    NU_START = NU_MEAN - NU_MEAN * NU_STD  # 0.0001
+    NU_END = NU_MEAN + NU_MEAN * NU_STD  # 0.1
+
+    ## prepare data with (?, 2)
+    data_1d_x = np.linspace(
+        X_IN, X_OUT, N_x, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_y = np.linspace(
+        Y_START, Y_END, N_y, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_nu = np.linspace(
+        NU_START, NU_END, N_p, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_2d_xy = (
+        np.array(np.meshgrid(data_1d_x, data_1d_y, data_1d_nu)).reshape(3, -1).T
+    )
+
+    # set model
+    model_u = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("u",), 3, 50, "swish")
+    model_v = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("v",), 3, 50, "swish")
+    model_p = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("p",), 3, 50, "swish")
+
+    class Transform:
+        def input_trans(self, input):
+            self.input = input
+            x, y = input["x"], input["y"]
+            nu = input["nu"]
+            b = 2 * np.pi / (X_OUT - X_IN)
+            c = np.pi * (X_IN + X_OUT) / (X_IN - X_OUT)
+            sin_x = X_IN * paddle.sin(b * x + c)
+            cos_x = X_IN * paddle.cos(b * x + c)
+            return {"sin(x)": sin_x, "cos(x)": cos_x, "y": y, "nu": nu}
+
+        def output_trans_u(self, input, out):
+            return {"u": out["u"] * (R**2 - self.input["y"] ** 2)}
+
+        def output_trans_v(self, input, out):
+            return {"v": (R**2 - self.input["y"] ** 2) * out["v"]}
+
+        def output_trans_p(self, input, out):
+            return {
+                "p": (
+                    (P_IN - P_OUT) * (X_OUT - self.input["x"]) / L
+                    + (X_IN - self.input["x"]) * (X_OUT - self.input["x"]) * out["p"]
+                )
+            }
+
+    transform = Transform()
+    model_u.register_input_transform(transform.input_trans)
+    model_v.register_input_transform(transform.input_trans)
+    model_p.register_input_transform(transform.input_trans)
+    model_u.register_output_transform(transform.output_trans_u)
+    model_v.register_output_transform(transform.output_trans_v)
+    model_p.register_output_transform(transform.output_trans_p)
+    model = ppsci.arch.ModelList((model_u, model_v, model_p))
+
+    # Validator vel
+    input_dict = {
+        "x": data_2d_xy[:, 0:1],
+        "y": data_2d_xy[:, 1:2],
+        "nu": data_2d_xy[:, 2:3],
+    }
+    u_analytical = np.zeros([N_y, N_x, N_p])
+    dP = P_IN - P_OUT
+
+    for i in range(N_p):
+        uy = (R**2 - data_1d_y**2) * dP / (2 * L * data_1d_nu[i] * RHO)
+        u_analytical[:, :, i] = np.tile(uy.reshape([N_y, 1]), N_x)
+
+    label_dict = {"u": np.ones_like(input_dict["x"])}
+    weight_dict = {"u": np.ones_like(input_dict["x"])}
+
+    # Validator KL
+    num_test = 500
+    data_1d_nu_distribution = np.random.normal(NU_MEAN, 0.2 * NU_MEAN, num_test)
+    data_2d_xy_test = (
+        np.array(
+            np.meshgrid((X_IN - X_OUT) / 2.0, 0, data_1d_nu_distribution), np.float32
+        )
+        .reshape(3, -1)
+        .T
+    )
+    input_dict_KL = {
+        "x": data_2d_xy_test[:, 0:1],
+        "y": data_2d_xy_test[:, 1:2],
+        "nu": data_2d_xy_test[:, 2:3],
+    }
+    u_max_a = (R**2) * dP / (2 * L * data_1d_nu_distribution * RHO)
+
+

2.3 完整代码

+
poiseuille_flow.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/Jianxun-Wang/LabelFree-DNN-Surrogate
+"""
+
+import copy
+import os
+from os import path as osp
+
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import checker
+
+if not checker.dynamic_import_to_globals("seaborn"):
+    raise ModuleNotFoundError("Please install seaborn with `pip install seaborn>=0.13.0`.")  # fmt: skip
+
+import seaborn as sns
+
+
+def train(cfg: DictConfig):
+    X_OUT = cfg.X_IN + cfg.L
+    Y_START = -cfg.R
+    Y_END = Y_START + 2 * cfg.R
+    NU_START = cfg.NU_MEAN - cfg.NU_MEAN * cfg.NU_STD  # 0.0001
+    NU_END = cfg.NU_MEAN + cfg.NU_MEAN * cfg.NU_STD  # 0.1
+
+    ## prepare data with (?, 2)
+    data_1d_x = np.linspace(
+        cfg.X_IN, X_OUT, cfg.N_x, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_y = np.linspace(
+        Y_START, Y_END, cfg.N_y, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_nu = np.linspace(
+        NU_START, NU_END, cfg.N_p, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+
+    data_2d_xy = (
+        np.array(np.meshgrid(data_1d_x, data_1d_y, data_1d_nu)).reshape(3, -1).T
+    )
+    data_2d_xy_shuffle = copy.deepcopy(data_2d_xy)
+    np.random.shuffle(data_2d_xy_shuffle)
+
+    input_x = data_2d_xy_shuffle[:, 0].reshape(data_2d_xy_shuffle.shape[0], 1)
+    input_y = data_2d_xy_shuffle[:, 1].reshape(data_2d_xy_shuffle.shape[0], 1)
+    input_nu = data_2d_xy_shuffle[:, 2].reshape(data_2d_xy_shuffle.shape[0], 1)
+
+    interior_geom = ppsci.geometry.PointCloud(
+        interior={"x": input_x, "y": input_y, "nu": input_nu},
+        coord_keys=("x", "y", "nu"),
+    )
+
+    # set model
+    model_u = ppsci.arch.MLP(**cfg.MODEL.u_net)
+    model_v = ppsci.arch.MLP(**cfg.MODEL.v_net)
+    model_p = ppsci.arch.MLP(**cfg.MODEL.p_net)
+
+    def input_trans(input):
+        x, y = input["x"], input["y"]
+        nu = input["nu"]
+        b = 2 * np.pi / (X_OUT - cfg.X_IN)
+        c = np.pi * (cfg.X_IN + X_OUT) / (cfg.X_IN - X_OUT)
+        sin_x = cfg.X_IN * paddle.sin(b * x + c)
+        cos_x = cfg.X_IN * paddle.cos(b * x + c)
+        return {"sin(x)": sin_x, "cos(x)": cos_x, "x": x, "y": y, "nu": nu}
+
+    def output_trans_u(input, out):
+        return {"u": out["u"] * (cfg.R**2 - input["y"] ** 2)}
+
+    def output_trans_v(input, out):
+        return {"v": (cfg.R**2 - input["y"] ** 2) * out["v"]}
+
+    def output_trans_p(input, out):
+        return {
+            "p": (
+                (cfg.P_IN - cfg.P_OUT) * (X_OUT - input["x"]) / cfg.L
+                + (cfg.X_IN - input["x"]) * (X_OUT - input["x"]) * out["p"]
+            )
+        }
+
+    model_u.register_input_transform(input_trans)
+    model_v.register_input_transform(input_trans)
+    model_p.register_input_transform(input_trans)
+    model_u.register_output_transform(output_trans_u)
+    model_v.register_output_transform(output_trans_v)
+    model_p.register_output_transform(output_trans_p)
+    model = ppsci.arch.ModelList((model_u, model_v, model_p))
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    # set euqation
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu="nu", rho=cfg.RHO, dim=2, time=False
+        )
+    }
+
+    # set constraint
+    ITERS_PER_EPOCH = int(
+        (cfg.N_x * cfg.N_y * cfg.N_p) / cfg.TRAIN.batch_size.pde_constraint
+    )
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom=interior_geom,
+        dataloader_cfg={
+            "dataset": "NamedArrayDataset",
+            "num_workers": 1,
+            "batch_size": cfg.TRAIN.batch_size.pde_constraint,
+            "iters_per_epoch": ITERS_PER_EPOCH,
+            "sampler": {
+                "name": "BatchSampler",
+                "shuffle": False,
+                "drop_last": False,
+            },
+        },
+        loss=ppsci.loss.MSELoss("mean"),
+        evenly=True,
+        name="EQ",
+    )
+    # wrap constraints together
+    constraint = {pde_constraint.name: pde_constraint}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        save_freq=cfg.TRAIN.save_freq,
+        equation=equation,
+    )
+    solver.train()
+
+
+def evaluate(cfg: DictConfig):
+    NU_MEAN = 0.001
+    NU_STD = 0.9
+    L = 1.0  # length of pipe
+    R = 0.05  # radius of pipe
+    RHO = 1  # density
+    P_OUT = 0  # pressure at the outlet of pipe
+    P_IN = 0.1  # pressure at the inlet of pipe
+    N_x = 10
+    N_y = 50
+    N_p = 50
+    X_IN = 0
+    X_OUT = X_IN + L
+    Y_START = -R
+    Y_END = Y_START + 2 * R
+    NU_START = NU_MEAN - NU_MEAN * NU_STD  # 0.0001
+    NU_END = NU_MEAN + NU_MEAN * NU_STD  # 0.1
+
+    ## prepare data with (?, 2)
+    data_1d_x = np.linspace(
+        X_IN, X_OUT, N_x, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_y = np.linspace(
+        Y_START, Y_END, N_y, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_1d_nu = np.linspace(
+        NU_START, NU_END, N_p, endpoint=True, dtype=paddle.get_default_dtype()
+    )
+    data_2d_xy = (
+        np.array(np.meshgrid(data_1d_x, data_1d_y, data_1d_nu)).reshape(3, -1).T
+    )
+
+    # set model
+    model_u = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("u",), 3, 50, "swish")
+    model_v = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("v",), 3, 50, "swish")
+    model_p = ppsci.arch.MLP(("sin(x)", "cos(x)", "y", "nu"), ("p",), 3, 50, "swish")
+
+    class Transform:
+        def input_trans(self, input):
+            self.input = input
+            x, y = input["x"], input["y"]
+            nu = input["nu"]
+            b = 2 * np.pi / (X_OUT - X_IN)
+            c = np.pi * (X_IN + X_OUT) / (X_IN - X_OUT)
+            sin_x = X_IN * paddle.sin(b * x + c)
+            cos_x = X_IN * paddle.cos(b * x + c)
+            return {"sin(x)": sin_x, "cos(x)": cos_x, "y": y, "nu": nu}
+
+        def output_trans_u(self, input, out):
+            return {"u": out["u"] * (R**2 - self.input["y"] ** 2)}
+
+        def output_trans_v(self, input, out):
+            return {"v": (R**2 - self.input["y"] ** 2) * out["v"]}
+
+        def output_trans_p(self, input, out):
+            return {
+                "p": (
+                    (P_IN - P_OUT) * (X_OUT - self.input["x"]) / L
+                    + (X_IN - self.input["x"]) * (X_OUT - self.input["x"]) * out["p"]
+                )
+            }
+
+    transform = Transform()
+    model_u.register_input_transform(transform.input_trans)
+    model_v.register_input_transform(transform.input_trans)
+    model_p.register_input_transform(transform.input_trans)
+    model_u.register_output_transform(transform.output_trans_u)
+    model_v.register_output_transform(transform.output_trans_v)
+    model_p.register_output_transform(transform.output_trans_p)
+    model = ppsci.arch.ModelList((model_u, model_v, model_p))
+
+    # Validator vel
+    input_dict = {
+        "x": data_2d_xy[:, 0:1],
+        "y": data_2d_xy[:, 1:2],
+        "nu": data_2d_xy[:, 2:3],
+    }
+    u_analytical = np.zeros([N_y, N_x, N_p])
+    dP = P_IN - P_OUT
+
+    for i in range(N_p):
+        uy = (R**2 - data_1d_y**2) * dP / (2 * L * data_1d_nu[i] * RHO)
+        u_analytical[:, :, i] = np.tile(uy.reshape([N_y, 1]), N_x)
+
+    label_dict = {"u": np.ones_like(input_dict["x"])}
+    weight_dict = {"u": np.ones_like(input_dict["x"])}
+
+    # Validator KL
+    num_test = 500
+    data_1d_nu_distribution = np.random.normal(NU_MEAN, 0.2 * NU_MEAN, num_test)
+    data_2d_xy_test = (
+        np.array(
+            np.meshgrid((X_IN - X_OUT) / 2.0, 0, data_1d_nu_distribution), np.float32
+        )
+        .reshape(3, -1)
+        .T
+    )
+    input_dict_KL = {
+        "x": data_2d_xy_test[:, 0:1],
+        "y": data_2d_xy_test[:, 1:2],
+        "nu": data_2d_xy_test[:, 2:3],
+    }
+    u_max_a = (R**2) * dP / (2 * L * data_1d_nu_distribution * RHO)
+    label_dict_KL = {"u": np.ones_like(input_dict_KL["x"])}
+    weight_dict_KL = {"u": np.ones_like(input_dict_KL["x"])}
+
+    class Cross_section_velocity_profile_metric(ppsci.metric.base.Metric):
+        def __init__(self, keep_batch: bool = False):
+            super().__init__(keep_batch)
+
+        @paddle.no_grad()
+        def forward(self, output_dict, label_dict):
+            u_pred = output_dict["u"].numpy().reshape(N_y, N_x, N_p)
+            metric_dict = {}
+            for nu in range(N_p):
+                err = (
+                    u_analytical[:, int(round(N_x / 2)), nu]
+                    - u_pred[:, int(round(N_x / 2)), nu]
+                )
+                metric_dict[f"nu = {data_1d_nu[nu]:.2g}"] = np.abs(err).sum()
+            return metric_dict
+
+    # Kullback-Leibler Divergence
+    class KL_divergence(ppsci.metric.base.Metric):
+        def __init__(self, keep_batch: bool = False):
+            super().__init__(keep_batch)
+
+        @paddle.no_grad()
+        def forward(self, output_dict, label_dict):
+            u_max_pred = output_dict["u"].numpy().flatten()
+            import scipy
+
+            print(f"KL = {scipy.stats.entropy(u_max_a, u_max_pred)}")
+            return {"KL divergence": scipy.stats.entropy(u_max_a, u_max_pred)}
+
+    dataset_vel = {
+        "name": "NamedArrayDataset",
+        "input": input_dict,
+        "label": label_dict,
+        "weight": weight_dict,
+    }
+    dataset_kl = {
+        "name": "NamedArrayDataset",
+        "input": input_dict_KL,
+        "label": label_dict_KL,
+        "weight": weight_dict_KL,
+    }
+    eval_cfg = {
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": False,
+            "drop_last": False,
+        },
+        "batch_size": 2000,
+    }
+    eval_cfg["dataset"] = dataset_vel
+    velocity_validator = ppsci.validate.SupervisedValidator(
+        eval_cfg,
+        ppsci.loss.MSELoss("mean"),
+        {"u": lambda out: out["u"]},
+        {"Cross_section_velocity_profile_MAE": Cross_section_velocity_profile_metric()},
+        name="Cross_section_velocity_profile_MAE",
+    )
+    eval_cfg["dataset"] = dataset_kl
+    kl_validator = ppsci.validate.SupervisedValidator(
+        eval_cfg,
+        ppsci.loss.MSELoss("mean"),
+        {"u": lambda out: out["u"]},
+        {"Cross_section_velocity_profile_MAE": KL_divergence()},
+        name="KL_divergence",
+    )
+    validator = {
+        velocity_validator.name: velocity_validator,
+        kl_validator.name: kl_validator,
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    solver.eval()
+
+    output_dict = solver.predict(input_dict, return_numpy=True)
+    u_pred = output_dict["u"].reshape(N_y, N_x, N_p)
+    fontsize = 16
+    idx_X = int(round(N_x / 2))  # pipe velocity section at L/2
+    nu_index = [3, 6, 9, 12, 14, 20, 49]  # pick 7 nu samples
+    ytext = [0.55, 0.5, 0.4, 0.28, 0.1, 0.05, 0.001]  # text y position
+
+    # Plot
+    PLOT_DIR = osp.join(cfg.output_dir, "visu")
+    os.makedirs(PLOT_DIR, exist_ok=True)
+    plt.figure(1)
+    plt.clf()
+    for idxP in range(len(nu_index)):
+        ax1 = plt.subplot(111)
+        plt.plot(
+            data_1d_y,
+            u_analytical[:, idx_X, nu_index[idxP]],
+            color="darkblue",
+            linestyle="-",
+            lw=3.0,
+            alpha=1.0,
+        )
+        plt.plot(
+            data_1d_y,
+            u_pred[:, idx_X, nu_index[idxP]],
+            color="red",
+            linestyle="--",
+            dashes=(5, 5),
+            lw=2.0,
+            alpha=1.0,
+        )
+        plt.text(
+            -0.012,
+            ytext[idxP],
+            rf"$\nu = $ {data_1d_nu[nu_index[idxP]]:.2g}",
+            {"color": "k", "fontsize": fontsize - 4},
+        )
+
+    plt.ylabel(r"$u(y)$", fontsize=fontsize)
+    plt.xlabel(r"$y$", fontsize=fontsize)
+    ax1.tick_params(axis="x", labelsize=fontsize)
+    ax1.tick_params(axis="y", labelsize=fontsize)
+    ax1.set_xlim([-0.05, 0.05])
+    ax1.set_ylim([0.0, 0.62])
+    plt.savefig(osp.join(PLOT_DIR, "pipe_uProfiles.png"), bbox_inches="tight")
+
+    # Distribution of center velocity
+    # Predicted result
+    input_dict_test = {
+        "x": data_2d_xy_test[:, 0:1],
+        "y": data_2d_xy_test[:, 1:2],
+        "nu": data_2d_xy_test[:, 2:3],
+    }
+    output_dict_test = solver.predict(input_dict_test, return_numpy=True)
+    u_max_pred = output_dict_test["u"]
+
+    # Analytical result, y = 0
+    u_max_a = (R**2) * dP / (2 * L * data_1d_nu_distribution * RHO)
+
+    # Plot
+    plt.figure(2)
+    plt.clf()
+    ax1 = plt.subplot(111)
+    sns.kdeplot(
+        u_max_a,
+        fill=True,
+        color="black",
+        label="Analytical",
+        linestyle="-",
+        linewidth=3,
+    )
+    sns.kdeplot(
+        u_max_pred,
+        fill=False,
+        color="red",
+        label="DNN",
+        linestyle="--",
+        linewidth=3.5,
+    )
+    plt.legend(prop={"size": fontsize})
+    plt.xlabel(r"$u_c$", fontsize=fontsize)
+    plt.ylabel(r"PDF", fontsize=fontsize)
+    ax1.tick_params(axis="x", labelsize=fontsize)
+    ax1.tick_params(axis="y", labelsize=fontsize)
+    plt.savefig(osp.join(PLOT_DIR, "pipe_unformUQ.png"), bbox_inches="tight")
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="poiseuille_flow.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

2.4 结果展示

+
+

laplace 2d +

+
(左)在 x=0 截面速度 u(y) 随 y 在四种不同的动力粘性系数采样下的曲线和解析解的对比 (右)当我们选取截断高斯分布的动力粘性系数 nu 采样(均值为 nu=0.001, 方差 sigma​=2.67 x 10e−4),中心处速度的概率密度函数和解析解对比
+
+

DNN代理模型的结果如左图所示,和泊肃叶流动的精确解(论文公式13)进行比较:

+
\[ +u_a = \dfrac{\delta p}{2 \nu \rho L} + (\dfrac{d^2}{4} - y^2) +\]
+

公式和图片中的 \(y\) 表示展向坐标,\(\delta p\),从图片中我们可以观察到DNN预测的,4种不同粘度采样下的速度曲线(红色虚线),几乎完美符合解析解的速度曲线(蓝色实线),其中,4个case的雷诺数(\(Re\))分别为283,121,33,3。实际上,只要雷诺数适中,DNN能精确预测任意给定动力学粘性系数的管道流。

+

右图展示了中心线(x方向管道中心)速度,在给定动力学粘性系数(高斯分布)下的不确定性。动力学粘性系数的高斯分布,平均值为\(1e^{-3}\),方差为\(2.67e^{-4}\),这样保证了动力学粘性系数是一个正随机变量。此外,这个高斯分布的区间为\((0,+\infty)\),概率密度函数为:

+
\[ +f(\nu ; \bar{\nu}, \sigma_{\nu}) = \dfrac{\dfrac{1}{\sigma_{\nu}} N(\dfrac{(\nu - \bar{\nu})}{\sigma_{\nu}})}{1 - \phi(-\dfrac{\bar{\nu}}{\sigma_{\nu}})} +\]
+

更多细节请参考论文第九页

+

3. 案例二: Aneurysm Flow

+

3.1 问题定义

+

本文主要研究了两种类型的典型血管流(具有标准化的血管几何形状),狭窄流和动脉瘤流。 +狭窄血流是指流过血管的血流,其中血管壁变窄和再扩张。 血管的这种局部限制与许多心血管疾病有关,例如动脉硬化、中风和心脏病发作 。 +动脉瘤内的血管血流,即由于血管壁薄弱导致的动脉扩张,称为动脉瘤血流。 动脉瘤破裂可能导致危及生命的情况,例如,由于脑动脉瘤破裂引起的蛛网膜下腔出血 (SAH),而血液动力学的研究可以提高诊断和对动脉瘤进展和破裂的基本了解 。

+

虽然现实的血管几何形状通常是不规则和复杂的,包括曲率、分叉和连接点,但这里研究理想化的狭窄和动脉瘤模型以进行概念验证。 即,狭窄血管和动脉瘤血管都被理想化为具有不同横截面半径的轴对称管,其由以下函数参数化,

+

质量守恒:

+
\[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} + \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} + \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) +\]
+

我们只关注这种完全发展的流动并且在边界施加了无滑移边界条件。与传统PINNs方法不同的是,我们将无滑动边界条件通过速度函数假设的方式强制施加在边界上: +对于流体域边界和流体域内部圆周边界,则需施加 Dirichlet 边界条件:

+
+

pipe +

+
流场示意图
+
+

流体域入口边界:

+
\[ +p=0.1 +\]
+

流体域出口边界:

+
\[ +p=0 +\]
+

流体域上下边界:

+
\[ +u=0, v=0 +\]
+

3.2 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.2.1 模型构建

+

在本案例中,每一个已知的坐标点和几何放大系数 \((x, y, scale)\) 都有自身的横向速度 \(u\)、纵向速度 \(v\)、压力 \(p\) +三个待求解的未知量,我们在这里使用比较简单的三个 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y, scale)\)\((u, v, p)\) 的映射函数 \(f_1, f_2, f_3: \mathbb{R}^3 \to \mathbb{R}^3\) ,即:

+
\[ +u= transform_{output}(f_1(transform_{input}(x, y, scale))) +\]
+
\[ +v= transform_{output}(f_2(transform_{input}(x, y, scale))) +\]
+
\[ +p= transform_{output}(f_3(transform_{input}(x, y, scale))) +\]
+

上式中 \(f_1, f_2, f_3\) 即为 MLP 模型本身,\(transform_{input}, transform_{output}\), 表示施加额外的结构化自定义层,用于施加约束和链接输入,用 PaddleScience 代码表示如下:

+
class Transform:
+    def __init__(self) -> None:
+        pass
+
+    def output_transform_u(self, in_, out):
+        x, y, scale = in_["x"], in_["y"], in_["scale"]
+        r_func = (
+            scale
+            / np.sqrt(2 * np.pi * SIGMA**2)
+            * paddle.exp(-((x - mu) ** 2) / (2 * SIGMA**2))
+        )
+        self.h = R_INLET - r_func
+        u = out["u"]
+        # The no-slip condition of velocity on the wall
+        return {"u": u * (self.h**2 - y**2)}
+
+    def output_transform_v(self, in_, out):
+        y = in_["y"]
+        v = out["v"]
+        # The no-slip condition of velocity on the wall
+        return {"v": (self.h**2 - y**2) * v}
+
+    def output_transform_p(self, in_, out):
+        x = in_["x"]
+        p = out["p"]
+        # The pressure inlet [p_in = 0.1] and outlet [p_out = 0]
+        return {
+            "p": ((P_IN - P_OUT) * (X_OUT - x) / L + (X_IN - x) * (X_OUT - x) * p)
+        }
+
+transform = Transform()
+model_1.register_output_transform(transform.output_transform_u)
+model_2.register_output_transform(transform.output_transform_v)
+model_3.register_output_transform(transform.output_transform_p)
+model = ppsci.arch.ModelList((model_1, model_2, model_3))
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ["x"、 "y"、 "scale"],输出变量名是 ["u"、 "v"、 "p"],这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了三个拥有 3 层隐藏神经元和 1 层输出层神经元的神经网络,每层神经元数为 20,使用 "silu" 作为激活函数的神经网络模型 model_1 model_2 model_3

+

此外,使用kaiming normal方法对权重和偏置初始化。

+
def init_func(m):
+    if misc.typename(m) == "Linear":
+        ppsci.utils.initializer.kaiming_normal_(m.weight, reverse=True)
+
+model_1 = ppsci.arch.MLP(("x", "y", "scale"), ("u",), 3, 20, "silu")
+model_2 = ppsci.arch.MLP(("x", "y", "scale"), ("v",), 3, 20, "silu")
+model_3 = ppsci.arch.MLP(("x", "y", "scale"), ("p",), 3, 20, "silu")
+model_1.apply(init_func)
+model_2.apply(init_func)
+model_3.apply(init_func)
+
+

3.2.2 方程构建

+

由于本案例使用的是 Navier-Stokes 方程的2维稳态形式,因此可以直接使用 PaddleScience 内置的 NavierStokes

+
equation = {"NavierStokes": ppsci.equation.NavierStokes(NU, RHO, 2, False)}
+
+

在实例化 NavierStokes 类时需指定必要的参数:动力粘度 \(\nu = 0.001\), 流体密度 \(\rho = 1.0\)

+
37
+38
+39
+40
+41
# Physic properties
+P_OUT = 0  # pressure at the outlet of pipe
+P_IN = 0.1  # pressure at the inlet of pipe
+NU = 1e-3
+RHO = 1
+
+

3.2.3 计算域构建

+

本文中本案例的计算域和参数自变量\(scale\)numpy随机数生成的点云构成,因此可以直接使用 PaddleScience 内置的点云几何 PointCloud 组合成空间的 Geometry 计算域。

+
# Geometry
+L = 1
+X_IN = 0
+X_OUT = X_IN + L
+R_INLET = 0.05
+mu = 0.5 * (X_OUT - X_IN)
+x_initial = np.linspace(X_IN, X_OUT, 100, dtype=paddle.get_default_dtype()).reshape(
+    100, 1
+)
+x_20_copy = np.tile(x_initial, (20, 1))  # duplicate 20 times of x for dataloader
+SIGMA = 0.1
+SCALE_START = -0.02
+SCALE_END = 0
+scale_initial = np.linspace(
+    SCALE_START, SCALE_END, 50, endpoint=True, dtype=paddle.get_default_dtype()
+).reshape(50, 1)
+scale = np.tile(scale_initial, (len(x_20_copy), 1))
+x = np.array([np.tile(val, len(scale_initial)) for val in x_20_copy]).reshape(
+    len(scale), 1
+)
+
+# Axisymmetric boundary
+r_func = (
+    scale
+    / math.sqrt(2 * np.pi * SIGMA**2)
+    * np.exp(-((x - mu) ** 2) / (2 * SIGMA**2))
+)
+
+# Visualize stenosis(scale == 0.2)
+PLOT_DIR = osp.join(cfg.output_dir, "visu")
+os.makedirs(PLOT_DIR, exist_ok=True)
+y_up = (R_INLET - r_func) * np.ones_like(x)
+y_down = (-R_INLET + r_func) * np.ones_like(x)
+idx = np.where(scale == 0)  # plot vessel which scale is 0.2 by finding its indices
+plt.figure()
+plt.scatter(x[idx], y_up[idx])
+plt.scatter(x[idx], y_down[idx])
+plt.axis("equal")
+plt.savefig(osp.join(PLOT_DIR, "idealized_stenotic_vessel"), bbox_inches="tight")
+
+# Points and shuffle(for alignment)
+y = np.zeros([len(x), 1], dtype=paddle.get_default_dtype())
+for x0 in x_initial:
+    index = np.where(x[:, 0] == x0)[0]
+    # y is linear to scale, so we place linspace to get 1000 x, it corresponds to vessels
+    y[index] = np.linspace(
+        -max(y_up[index]),
+        max(y_up[index]),
+        len(index),
+        dtype=paddle.get_default_dtype(),
+    ).reshape(len(index), -1)
+
+idx = np.where(scale == 0)  # plot vessel which scale is 0.2 by finding its indices
+plt.figure()
+plt.scatter(x[idx], y[idx])
+plt.axis("equal")
+plt.savefig(osp.join(PLOT_DIR, "one_scale_sample"), bbox_inches="tight")
+interior_geom = ppsci.geometry.PointCloud(
+    interior={"x": x, "y": y, "scale": scale},
+    coord_keys=("x", "y", "scale"),
+)
+geom = {"interior": interior_geom}
+
+

3.2.4 约束构建

+

根据 3.1 问题定义 得到的公式和和边界条件,对应了在计算域中指导模型训练的几个约束条件,即:

+
    +
  • +

    施加在流体域内部点上的Navier-Stokes 方程约束

    +

    质量守恒:

    +
    \[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
    +

    \(x\) 动量守恒:

    +
    \[ +u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} +\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} - \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) = 0 +\]
    +

    \(y\) 动量守恒:

    +
    \[ +u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} +\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} - \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) = 0 +\]
    +

    为了方便获取中间变量,NavierStokes 类内部将上式左侧的结果分别命名为 continuity, momentum_x, momentum_y

    +
  • +
  • +

    施加在流体域入出口、流体域上下血管壁边界的的 Dirichlet 边界条件约束。作为本文创新点之一,此案例创新性的使用了结构化边界条件,即通过网络的输出层后面,增加一层公式层,来施加边界条件(公式在边界处值为零)。避免了数据点作为边界条件无法有效约束。统一使用用类函数Transform()进行初始化和管理。具体的推理过程为:

    +

    设狭窄缩放系数为\(A\):

    +
    \[ +R(x) = R_{0} - A\dfrac{1}{\sqrt{2\pi\sigma^2}}exp(-\dfrac{(x-\mu)^2}{2\sigma^2}) +\]
    +
    \[ +d = R(x) +\]
    +

    具体的修正函数带入后得到:

    +
    \[ +\hat{u} = (\dfrac{d^2}{4} - y^2) \tilde{u} +\]
    +
    \[ +\hat{v} = (\dfrac{d^2}{4} - y^2) \tilde{v} +\]
    +
    \[ +\hat{p} = \dfrac{x - x_{in}}{x_{out} - x_{in}}p_{out} + \dfrac{x_{out} - x}{x_{out} - x_{in}}p_{in} + (x - x_{in})(x_{out} - x) \tilde{p} +\]
    +
  • +
+

接下来使用 PaddleScience 内置的 InteriorConstraint 和模型Transform自定义层,构建上述两种约束条件。

+
    +
  • +

    内部点约束

    +

    以作用在流体域内部点上的 InteriorConstraint 为例,代码如下:

    +
    pde_constraint = ppsci.constraint.InteriorConstraint(
    +    equation["NavierStokes"].equations,
    +    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
    +    geom=geom["interior"],
    +    dataloader_cfg={
    +        "dataset": "NamedArrayDataset",
    +        "num_workers": 1,
    +        "batch_size": cfg.TRAIN.batch_size,
    +        "iters_per_epoch": int(x.shape[0] / cfg.TRAIN.batch_size),
    +        "sampler": {
    +            "name": "BatchSampler",
    +            "shuffle": True,
    +            "drop_last": False,
    +        },
    +    },
    +    loss=ppsci.loss.MSELoss("mean"),
    +    evenly=True,
    +    name="EQ",
    +)
    +constraint = {pde_constraint.name: pde_constraint}
    +
    +

    InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2.2 方程构建 章节中实例化好的 equation["NavierStokes"].equations

    +

    第二个参数是约束变量的目标值,在本问题中我们希望 Navier-Stokes 方程产生的三个中间结果 continuity, momentum_x, momentum_y 被优化至 0,因此将它们的目标值全部设为 0;

    +

    第三个参数是约束方程作用的计算域,此处填入在 3.2.3 计算域构建 章节实例化好的 interior_geom 即可;

    +

    第四个参数是在计算域上的采样配置,此处我们使用分批次数据点训练,因此 dataset 字段设置为 NamedArrayDatasetiters_per_epoch 也设置为 1,采样点数 batch_size 设为 128;

    +

    第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

    +

    第六个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

    +
  • +
+

3.2.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,使用400轮训练轮数,学习率设为 0.005。

+
33
+34
+35
+36
+37
+38
+39
+40
+41
+42
# training settings
+TRAIN:
+  epochs: 400
+  learning_rate: 1e-3
+  beta1: 0.9
+  beta2: 0.99
+  epsilon: 1e-15
+  batch_size: 50
+  pretrained_model_path: null
+  checkpoint_path: null
+
+

3.2.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
optimizer_1 = ppsci.optimizer.Adam(
+    cfg.TRAIN.learning_rate,
+    beta1=cfg.TRAIN.beta1,
+    beta2=cfg.TRAIN.beta2,
+    epsilon=cfg.TRAIN.epsilon,
+)(model_1)
+optimizer_2 = ppsci.optimizer.Adam(
+    cfg.TRAIN.learning_rate,
+    beta1=cfg.TRAIN.beta1,
+    beta2=cfg.TRAIN.beta2,
+    epsilon=cfg.TRAIN.epsilon,
+)(model_2)
+optimizer_3 = ppsci.optimizer.Adam(
+    cfg.TRAIN.learning_rate,
+    beta1=cfg.TRAIN.beta1,
+    beta2=cfg.TRAIN.beta2,
+    epsilon=cfg.TRAIN.epsilon,
+)(model_3)
+optimizer = ppsci.optimizer.OptimizerList((optimizer_1, optimizer_2, optimizer_3))
+
+

3.2.7 模型训练、评估与可视化(需要下载数据)

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后推理。

+
def evaluate(cfg: DictConfig):
+
+

3.3 完整代码

+
aneurysm_flow.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/Jianxun-Wang/LabelFree-DNN-Surrogate
+"""
+
+import math
+import os
+import os.path as osp
+
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+from ppsci.utils import misc
+
+paddle.framework.core.set_prim_eager_enabled(True)
+
+
+def train(cfg: DictConfig):
+    # Physic properties
+    P_OUT = 0  # pressure at the outlet of pipe
+    P_IN = 0.1  # pressure at the inlet of pipe
+    NU = 1e-3
+    RHO = 1
+
+    # Geometry
+    L = 1
+    X_IN = 0
+    X_OUT = X_IN + L
+    R_INLET = 0.05
+    mu = 0.5 * (X_OUT - X_IN)
+    x_initial = np.linspace(X_IN, X_OUT, 100, dtype=paddle.get_default_dtype()).reshape(
+        100, 1
+    )
+    x_20_copy = np.tile(x_initial, (20, 1))  # duplicate 20 times of x for dataloader
+    SIGMA = 0.1
+    SCALE_START = -0.02
+    SCALE_END = 0
+    scale_initial = np.linspace(
+        SCALE_START, SCALE_END, 50, endpoint=True, dtype=paddle.get_default_dtype()
+    ).reshape(50, 1)
+    scale = np.tile(scale_initial, (len(x_20_copy), 1))
+    x = np.array([np.tile(val, len(scale_initial)) for val in x_20_copy]).reshape(
+        len(scale), 1
+    )
+
+    # Axisymmetric boundary
+    r_func = (
+        scale
+        / math.sqrt(2 * np.pi * SIGMA**2)
+        * np.exp(-((x - mu) ** 2) / (2 * SIGMA**2))
+    )
+
+    # Visualize stenosis(scale == 0.2)
+    PLOT_DIR = osp.join(cfg.output_dir, "visu")
+    os.makedirs(PLOT_DIR, exist_ok=True)
+    y_up = (R_INLET - r_func) * np.ones_like(x)
+    y_down = (-R_INLET + r_func) * np.ones_like(x)
+    idx = np.where(scale == 0)  # plot vessel which scale is 0.2 by finding its indices
+    plt.figure()
+    plt.scatter(x[idx], y_up[idx])
+    plt.scatter(x[idx], y_down[idx])
+    plt.axis("equal")
+    plt.savefig(osp.join(PLOT_DIR, "idealized_stenotic_vessel"), bbox_inches="tight")
+
+    # Points and shuffle(for alignment)
+    y = np.zeros([len(x), 1], dtype=paddle.get_default_dtype())
+    for x0 in x_initial:
+        index = np.where(x[:, 0] == x0)[0]
+        # y is linear to scale, so we place linspace to get 1000 x, it corresponds to vessels
+        y[index] = np.linspace(
+            -max(y_up[index]),
+            max(y_up[index]),
+            len(index),
+            dtype=paddle.get_default_dtype(),
+        ).reshape(len(index), -1)
+
+    idx = np.where(scale == 0)  # plot vessel which scale is 0.2 by finding its indices
+    plt.figure()
+    plt.scatter(x[idx], y[idx])
+    plt.axis("equal")
+    plt.savefig(osp.join(PLOT_DIR, "one_scale_sample"), bbox_inches="tight")
+    interior_geom = ppsci.geometry.PointCloud(
+        interior={"x": x, "y": y, "scale": scale},
+        coord_keys=("x", "y", "scale"),
+    )
+    geom = {"interior": interior_geom}
+
+    def init_func(m):
+        if misc.typename(m) == "Linear":
+            ppsci.utils.initializer.kaiming_normal_(m.weight, reverse=True)
+
+    model_1 = ppsci.arch.MLP(("x", "y", "scale"), ("u",), 3, 20, "silu")
+    model_2 = ppsci.arch.MLP(("x", "y", "scale"), ("v",), 3, 20, "silu")
+    model_3 = ppsci.arch.MLP(("x", "y", "scale"), ("p",), 3, 20, "silu")
+    model_1.apply(init_func)
+    model_2.apply(init_func)
+    model_3.apply(init_func)
+
+    class Transform:
+        def __init__(self) -> None:
+            pass
+
+        def output_transform_u(self, in_, out):
+            x, y, scale = in_["x"], in_["y"], in_["scale"]
+            r_func = (
+                scale
+                / np.sqrt(2 * np.pi * SIGMA**2)
+                * paddle.exp(-((x - mu) ** 2) / (2 * SIGMA**2))
+            )
+            self.h = R_INLET - r_func
+            u = out["u"]
+            # The no-slip condition of velocity on the wall
+            return {"u": u * (self.h**2 - y**2)}
+
+        def output_transform_v(self, in_, out):
+            y = in_["y"]
+            v = out["v"]
+            # The no-slip condition of velocity on the wall
+            return {"v": (self.h**2 - y**2) * v}
+
+        def output_transform_p(self, in_, out):
+            x = in_["x"]
+            p = out["p"]
+            # The pressure inlet [p_in = 0.1] and outlet [p_out = 0]
+            return {
+                "p": ((P_IN - P_OUT) * (X_OUT - x) / L + (X_IN - x) * (X_OUT - x) * p)
+            }
+
+    transform = Transform()
+    model_1.register_output_transform(transform.output_transform_u)
+    model_2.register_output_transform(transform.output_transform_v)
+    model_3.register_output_transform(transform.output_transform_p)
+    model = ppsci.arch.ModelList((model_1, model_2, model_3))
+    optimizer_1 = ppsci.optimizer.Adam(
+        cfg.TRAIN.learning_rate,
+        beta1=cfg.TRAIN.beta1,
+        beta2=cfg.TRAIN.beta2,
+        epsilon=cfg.TRAIN.epsilon,
+    )(model_1)
+    optimizer_2 = ppsci.optimizer.Adam(
+        cfg.TRAIN.learning_rate,
+        beta1=cfg.TRAIN.beta1,
+        beta2=cfg.TRAIN.beta2,
+        epsilon=cfg.TRAIN.epsilon,
+    )(model_2)
+    optimizer_3 = ppsci.optimizer.Adam(
+        cfg.TRAIN.learning_rate,
+        beta1=cfg.TRAIN.beta1,
+        beta2=cfg.TRAIN.beta2,
+        epsilon=cfg.TRAIN.epsilon,
+    )(model_3)
+    optimizer = ppsci.optimizer.OptimizerList((optimizer_1, optimizer_2, optimizer_3))
+
+    equation = {"NavierStokes": ppsci.equation.NavierStokes(NU, RHO, 2, False)}
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom=geom["interior"],
+        dataloader_cfg={
+            "dataset": "NamedArrayDataset",
+            "num_workers": 1,
+            "batch_size": cfg.TRAIN.batch_size,
+            "iters_per_epoch": int(x.shape[0] / cfg.TRAIN.batch_size),
+            "sampler": {
+                "name": "BatchSampler",
+                "shuffle": True,
+                "drop_last": False,
+            },
+        },
+        loss=ppsci.loss.MSELoss("mean"),
+        evenly=True,
+        name="EQ",
+    )
+    constraint = {pde_constraint.name: pde_constraint}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        log_freq=cfg.log_freq,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=int(x.shape[0] / cfg.TRAIN.batch_size),
+        save_freq=cfg.save_freq,
+        equation=equation,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+    )
+    solver.train()
+
+
+def evaluate(cfg: DictConfig):
+    PLOT_DIR = osp.join(cfg.output_dir, "visu")
+    os.makedirs(PLOT_DIR, exist_ok=True)
+
+    # Physic properties
+    P_OUT = 0  # pressure at the outlet of pipe
+    P_IN = 0.1  # pressure at the inlet of pipe
+    NU = 1e-3
+
+    # Geometry
+    L = 1
+    X_IN = 0
+    X_OUT = X_IN + L
+    R_INLET = 0.05
+    mu = 0.5 * (X_OUT - X_IN)
+    SIGMA = 0.1
+
+    def init_func(m):
+        if misc.typename(m) == "Linear":
+            ppsci.utils.initializer.kaiming_normal_(m.weight, reverse=True)
+
+    model_1 = ppsci.arch.MLP(("x", "y", "scale"), ("u",), 3, 20, "silu")
+    model_2 = ppsci.arch.MLP(("x", "y", "scale"), ("v",), 3, 20, "silu")
+    model_3 = ppsci.arch.MLP(("x", "y", "scale"), ("p",), 3, 20, "silu")
+    model_1.apply(init_func)
+    model_2.apply(init_func)
+    model_3.apply(init_func)
+
+    class Transform:
+        def __init__(self) -> None:
+            pass
+
+        def output_transform_u(self, in_, out):
+            x, y, scale = in_["x"], in_["y"], in_["scale"]
+            r_func = (
+                scale
+                / np.sqrt(2 * np.pi * SIGMA**2)
+                * paddle.exp(-((x - mu) ** 2) / (2 * SIGMA**2))
+            )
+            self.h = R_INLET - r_func
+            u = out["u"]
+            # The no-slip condition of velocity on the wall
+            return {"u": u * (self.h**2 - y**2)}
+
+        def output_transform_v(self, in_, out):
+            y = in_["y"]
+            v = out["v"]
+            # The no-slip condition of velocity on the wall
+            return {"v": (self.h**2 - y**2) * v}
+
+        def output_transform_p(self, in_, out):
+            x = in_["x"]
+            p = out["p"]
+            # The pressure inlet [p_in = 0.1] and outlet [p_out = 0]
+            return {
+                "p": ((P_IN - P_OUT) * (X_OUT - x) / L + (X_IN - x) * (X_OUT - x) * p)
+            }
+
+    transform = Transform()
+    model_1.register_output_transform(transform.output_transform_u)
+    model_2.register_output_transform(transform.output_transform_v)
+    model_3.register_output_transform(transform.output_transform_p)
+    model = ppsci.arch.ModelList((model_1, model_2, model_3))
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    def model_predict(
+        x: np.ndarray, y: np.ndarray, scale: np.ndarray, solver: ppsci.solver.Solver
+    ):
+        xt = paddle.to_tensor(x)
+        yt = paddle.to_tensor(y)
+        scalet = paddle.full_like(xt, scale)
+        input_dict = {"x": xt, "y": yt, "scale": scalet}
+        output_dict = solver.predict(input_dict, batch_size=100, return_numpy=True)
+        return output_dict
+
+    scale_test = np.load("./data/aneurysm_scale0005to002_eval0to002mean001_3sigma.npz")[
+        "scale"
+    ]
+    CASE_SELECTED = [1, 151, 486]
+    PLOT_X = 0.8
+    PLOT_Y = 0.06
+    FONTSIZE = 14
+    axis_limit = [0, 1, -0.15, 0.15]
+    path = "./data/cases/"
+    D_P = 0.1
+    error_u = []
+    error_v = []
+    N_CL = 200  # number of sampling points in centerline (confused about centerline, but the paper did not explain)
+    x_centerline = np.linspace(
+        X_IN, X_OUT, N_CL, dtype=paddle.get_default_dtype()
+    ).reshape(N_CL, 1)
+    for case_id in CASE_SELECTED:
+        scale = scale_test[case_id - 1]
+        data_CFD = np.load(osp.join(path, f"{case_id}CFD_contour.npz"))
+        x = data_CFD["x"].astype(paddle.get_default_dtype())
+        y = data_CFD["y"].astype(paddle.get_default_dtype())
+        u_cfd = data_CFD["U"].astype(paddle.get_default_dtype())
+        # p_cfd = data_CFD["P"].astype(paddle.get_default_dtype()) # missing data
+
+        n = len(x)
+        output_dict = model_predict(
+            x.reshape(n, 1),
+            y.reshape(n, 1),
+            np.full((n, 1), scale, dtype=paddle.get_default_dtype()),
+            solver,
+        )
+        u, v, _ = (
+            output_dict["u"],
+            output_dict["v"],
+            output_dict["p"],
+        )
+        w = np.zeros_like(u)
+        u_vec = np.concatenate([u, v, w], axis=1)
+        error_u.append(
+            np.linalg.norm(u_vec[:, 0] - u_cfd[:, 0]) / (D_P * len(u_vec[:, 0]))
+        )
+        error_v.append(
+            np.linalg.norm(u_vec[:, 1] - u_cfd[:, 1]) / (D_P * len(u_vec[:, 0]))
+        )
+
+        # Stream-wise velocity component u
+        plt.figure()
+        plt.subplot(212)
+        plt.scatter(x, y, c=u_vec[:, 0], vmin=min(u_cfd[:, 0]), vmax=max(u_cfd[:, 0]))
+        plt.text(PLOT_X, PLOT_Y, r"DNN", {"color": "b", "fontsize": FONTSIZE})
+        plt.axis(axis_limit)
+        plt.colorbar()
+        plt.subplot(211)
+        plt.scatter(x, y, c=u_cfd[:, 0], vmin=min(u_cfd[:, 0]), vmax=max(u_cfd[:, 0]))
+        plt.colorbar()
+        plt.text(PLOT_X, PLOT_Y, r"CFD", {"color": "b", "fontsize": FONTSIZE})
+        plt.axis(axis_limit)
+        plt.savefig(
+            osp.join(PLOT_DIR, f"{case_id}_scale_{scale}_uContour_test.png"),
+            bbox_inches="tight",
+        )
+
+        # Span-wise velocity component v
+        plt.figure()
+        plt.subplot(212)
+        plt.scatter(x, y, c=u_vec[:, 1], vmin=min(u_cfd[:, 1]), vmax=max(u_cfd[:, 1]))
+        plt.text(PLOT_X, PLOT_Y, r"DNN", {"color": "b", "fontsize": FONTSIZE})
+        plt.axis(axis_limit)
+        plt.colorbar()
+        plt.subplot(211)
+        plt.scatter(x, y, c=u_cfd[:, 1], vmin=min(u_cfd[:, 1]), vmax=max(u_cfd[:, 1]))
+        plt.colorbar()
+        plt.text(PLOT_X, PLOT_Y, r"CFD", {"color": "b", "fontsize": FONTSIZE})
+        plt.axis(axis_limit)
+        plt.savefig(
+            osp.join(PLOT_DIR, f"{case_id}_scale_{scale}_vContour_test.png"),
+            bbox_inches="tight",
+        )
+        plt.close("all")
+
+        # Centerline wall shear profile tau_c (downside)
+        data_CFD_wss = np.load(osp.join(path, f"{case_id}CFD_wss.npz"))
+        x_initial = data_CFD_wss["x"]
+        wall_shear_mag_up = data_CFD_wss["wss"]
+
+        D_H = 0.001  # The span-wise distance is approximately the height of the wall
+        r_cl = (
+            scale
+            / np.sqrt(2 * np.pi * SIGMA**2)
+            * np.exp(-((x_centerline - mu) ** 2) / (2 * SIGMA**2))
+        )
+        y_wall = (-R_INLET + D_H) * np.ones_like(x_centerline) + r_cl
+        output_dict_wss = model_predict(
+            x_centerline,
+            y_wall,
+            np.full((N_CL, 1), scale, dtype=paddle.get_default_dtype()),
+            solver,
+        )
+        v_cl_total = np.zeros_like(
+            x_centerline
+        )  # assuming normal velocity along the wall is zero
+        u_cl = output_dict_wss["u"]
+        v_cl = output_dict_wss["v"]
+        v_cl_total = np.sqrt(u_cl**2 + v_cl**2)
+        tau_c = NU * v_cl_total / D_H
+        plt.figure()
+        plt.plot(
+            x_initial,
+            wall_shear_mag_up,
+            label="CFD",
+            color="darkblue",
+            linestyle="-",
+            lw=3.0,
+            alpha=1.0,
+        )
+        plt.plot(
+            x_initial,
+            tau_c,
+            label="DNN",
+            color="red",
+            linestyle="--",
+            dashes=(5, 5),
+            lw=2.0,
+            alpha=1.0,
+        )
+        plt.xlabel(r"x", fontsize=16)
+        plt.ylabel(r"$\tau_{c}$", fontsize=16)
+        plt.legend(prop={"size": 16})
+        plt.savefig(
+            osp.join(PLOT_DIR, f"{case_id}_nu__{scale}_wallshear_test.png"),
+            bbox_inches="tight",
+        )
+        plt.close("all")
+    logger.message(
+        f"Table 1 : Aneurysm - Geometry error u : {sum(error_u) / len(error_u): .3e}"
+    )
+    logger.message(
+        f"Table 1 : Aneurysm - Geometry error v : {sum(error_v) / len(error_v): .3e}"
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="aneurysm_flow.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

3.4 结果展示

+
+

pipe
+ pipe
+ pipe +

+
第一行为x方向速度,第二行为y方向速度,第三行为壁面剪切应力曲线
+
+

图片展示了对于几何变化的动脉瘤流动的求解能力,其中训练是通过,对几何缩放系数\(A\)\(0\)\(-2e^{-2}\)区间采样进行的。三种不同几何的流场预测如图所示,动脉瘤的大小从左到右增加,流动速度在血管扩张区域减小,在动脉瘤中心处衰减最多。从前两行图片可以看出CFD结果和模型预测结果符合较好。对于WSS壁面剪切应力,曲线随着几何的变化也被模型精确捕获。

+

更多细节参考论文13页。

+

4. 参考文献

+

参考文献: Surrogate modeling for fluid flows based on physics-constrained deep learning without simulation data

+

参考代码: LabelFree-DNN-Surrogate

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/laplace2d/index.html b/zh/examples/laplace2d/index.html new file mode 100644 index 0000000000..c1a561d9b5 --- /dev/null +++ b/zh/examples/laplace2d/index.html @@ -0,0 +1,4426 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Laplace2D - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-Laplace

+

AI Studio快速体验

+
+
+
+
python laplace2d.py
+
+
+
+
python laplace2d.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/laplace2d/laplace2d_pretrained.pdparams
+
+
+
+
python laplace2d.py mode=export
+
+
+
+
python laplace2d.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
laplace2d_pretrained.pdparamsloss(MSE_Metric): 0.00002
MSE.u(MSE_Metric): 0.00002
+

1. 背景简介

+

拉普拉斯方程由法国数学家拉普拉斯首先提出而得名,该方程在许多领域都有重要应用,例如电磁学、天文学和流体力学等。在实际应用中,拉普拉斯方程的求解往往是一个复杂的数学问题。对于一些具有特定边界条件和初始条件的实际问题,可以通过特定的数值方法(如有限元方法、有限差分方法等)来求解拉普拉斯方程。对于一些复杂的问题,可能需要采用更高级的数值方法或者借助高性能计算机进行计算。

+

本案例通过深度学习的方式对拉普拉斯方程的2维形式进行求解。

+

2. 问题定义

+

拉普拉斯方程(2维形式):

+
\[ +\dfrac{\partial^{2} u}{\partial x^{2}} + \dfrac{\partial^{2} u}{\partial y^{2}} = 0, x \in (0, 1), y \in (0, 1) +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 2D-Laplace 问题中,每一个已知的坐标点 \((x, y)\) 都有对应的待求解的未知量 \(u\) +,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((x, y)\)\((u)\) 的映射函数 \(f: \mathbb{R}^2 \to \mathbb{R}^1\) ,即:

+
\[ +u = f(x, y) +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("x", "y"),输出变量名是 ("u",),这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数,我们就实例化出了一个拥有 5 层隐藏神经元,每层神经元数为 20 的神经网络模型 model

+

3.2 方程构建

+

由于 2D-Laplace 使用的是 Laplace 方程的2维形式,因此可以直接使用 PaddleScience 内置的 Laplace,指定该类的参数 dim 为2。

+
# set equation
+equation = {"laplace": ppsci.equation.Laplace(dim=2)}
+
+

3.3 计算域构建

+

本文中 2D Laplace 问题作用在以 (0.0, 0.0), (1.0, 1.0) 为对角线的二维矩形区域, +因此可以直接使用 PaddleScience 内置的空间几何 Rectangle 作为计算域。

+
29
+30
+31
+32
+33
+34
# set geometry
+geom = {
+    "rect": ppsci.geometry.Rectangle(
+        cfg.DIAGONAL_COORD.xmin, cfg.DIAGONAL_COORD.xmax
+    )
+}
+
+

3.4 约束构建

+

在本案例中,我们使用了两个约束条件在计算域中指导模型的训练分别是作用于采样点上的 Laplace 方程约束和作用于边界点上的约束。

+

在定义约束之前,需要给每一种约束指定采样点个数,表示每一种约束在其对应计算域内采样数据的数量,以及通用的采样配置。

+
NPOINT_INTERIOR: 9801
+NPOINT_BC: 400
+
+

3.4.1 内部点约束

+

以作用在内部点上的 InteriorConstraint 为例,代码如下:

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
# set constraint
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["laplace"].equations,
+    {"laplace": 0},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_TOTAL},
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    name="EQ",
+)
+
+

InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["laplace"].equations

+

第二个参数是约束变量的目标值,在本问题中我们希望 Laplace 方程产生的结果 laplace 被优化至 0,因此将它的目标值全设为 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["rect"] 即可;

+

第四个参数是在计算域上的采样配置,此处我们使用全量数据点训练,因此 dataset 字段设置为 "IterableNamedArrayDataset" 且 iters_per_epoch 也设置为 1,采样点数 batch_size 设为 10201(表示99x99的等间隔网格加400个边界点);

+

第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "sum",即我们会将参与计算的所有数据点产生的损失项求和;

+

第六个参数是选择是否在计算域上进行等间隔采样,此处我们选择开启等间隔采样,这样能让训练点均匀分布在计算域上,有利于训练收敛;

+

第七个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

+

3.4.2 边界约束

+

同理,我们还需要构建矩形的四个边界的约束。但与构建 InteriorConstraint 约束不同的是,由于作用区域是边界,因此我们使用 BoundaryConstraint 类,代码如下:

+
60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
bc = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+    {"u": u_solution_func},
+    geom["rect"],
+    {**train_dataloader_cfg, "batch_size": cfg.NPOINT_BC},
+    ppsci.loss.MSELoss("sum"),
+    name="BC",
+)
+# wrap constraints together
+constraint = {
+    pde_constraint.name: pde_constraint,
+    bc.name: bc,
+}
+
+

BoundaryConstraint 类第一个参数表示我们直接对网络模型的输出结果 out["u"] 作为程序运行时的约束对象;

+

第二个参数是指我们约束对象的真值如何获得,这里我们直接通过其解析解进行计算,定义解析解的代码如下:

+
36
+37
+38
+39
+40
# compute ground truth function
+def u_solution_func(out):
+    """compute ground truth for u as label data"""
+    x, y = out["x"], out["y"]
+    return np.cos(x) * np.cosh(y)
+
+

BoundaryConstraint 类其他参数的含义与 InteriorConstraint 基本一致,这里不再介绍。

+

3.5 超参数设定

+

接下来我们需要在配置文件中指定训练轮数,此处我们按实验经验,使用两万轮训练轮数,评估间隔为两百轮。

+
45
+46
+47
+48
+49
+50
# training settings
+TRAIN:
+  epochs: 20000
+  iters_per_epoch: 1
+  eval_during_train: true
+  eval_freq: 200
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
# set validator
+mse_metric = ppsci.validate.GeometryValidator(
+    {"u": lambda out: out["u"]},
+    {"u": u_solution_func},
+    geom["rect"],
+    {
+        "dataset": "IterableNamedArrayDataset",
+        "total_size": NPOINT_TOTAL,
+    },
+    ppsci.loss.MSELoss(),
+    evenly=True,
+    metric={"MSE": ppsci.metric.MSE()},
+    with_initial=True,
+    name="MSE_Metric",
+)
+validator = {mse_metric.name: mse_metric}
+
+

3.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文中的输出数据是一个区域内的二维点集,因此我们只需要将评估的输出数据保存成 vtu格式 文件,最后用可视化软件打开查看即可。代码如下:

+
# set visualizer(optional)
+vis_points = geom["rect"].sample_interior(NPOINT_TOTAL, evenly=True)
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerVtu(
+        vis_points,
+        {"u": lambda d: d["u"]},
+        num_timestamps=1,
+        prefix="result_u",
+    )
+}
+
+

3.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
laplace2d.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"laplace": ppsci.equation.Laplace(dim=2)}
+
+    # set geometry
+    geom = {
+        "rect": ppsci.geometry.Rectangle(
+            cfg.DIAGONAL_COORD.xmin, cfg.DIAGONAL_COORD.xmax
+        )
+    }
+
+    # compute ground truth function
+    def u_solution_func(out):
+        """compute ground truth for u as label data"""
+        x, y = out["x"], out["y"]
+        return np.cos(x) * np.cosh(y)
+
+    # set train dataloader config
+    train_dataloader_cfg = {
+        "dataset": "IterableNamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+
+    NPOINT_TOTAL = cfg.NPOINT_INTERIOR + cfg.NPOINT_BC
+
+    # set constraint
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["laplace"].equations,
+        {"laplace": 0},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_TOTAL},
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        name="EQ",
+    )
+    bc = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["rect"],
+        {**train_dataloader_cfg, "batch_size": cfg.NPOINT_BC},
+        ppsci.loss.MSELoss("sum"),
+        name="BC",
+    )
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        bc.name: bc,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    mse_metric = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["rect"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": NPOINT_TOTAL,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="MSE_Metric",
+    )
+    validator = {mse_metric.name: mse_metric}
+
+    # set visualizer(optional)
+    vis_points = geom["rect"].sample_interior(NPOINT_TOTAL, evenly=True)
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"]},
+            num_timestamps=1,
+            prefix="result_u",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"laplace": ppsci.equation.Laplace(dim=2)}
+
+    # set geometry
+    geom = {
+        "rect": ppsci.geometry.Rectangle(
+            cfg.DIAGONAL_COORD.xmin, cfg.DIAGONAL_COORD.xmax
+        )
+    }
+
+    # compute ground truth function
+    def u_solution_func(out):
+        """compute ground truth for u as label data"""
+        x, y = out["x"], out["y"]
+        return np.cos(x) * np.cosh(y)
+
+    NPOINT_TOTAL = cfg.NPOINT_INTERIOR + cfg.NPOINT_BC
+
+    # set validator
+    mse_metric = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["rect"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": NPOINT_TOTAL,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="MSE_Metric",
+    )
+    validator = {mse_metric.name: mse_metric}
+
+    # set visualizer(optional)
+    vis_points = geom["rect"].sample_interior(NPOINT_TOTAL, evenly=True)
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"]},
+            num_timestamps=1,
+            prefix="result_u",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    geom = {
+        "rect": ppsci.geometry.Rectangle(
+            cfg.DIAGONAL_COORD.xmin, cfg.DIAGONAL_COORD.xmax
+        )
+    }
+    NPOINT_TOTAL = cfg.NPOINT_INTERIOR + cfg.NPOINT_BC
+    input_dict = geom["rect"].sample_interior(NPOINT_TOTAL, evenly=True)
+
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    # save result
+    ppsci.visualize.save_vtu_from_dict(
+        "./laplace2d_pred.vtu",
+        {**input_dict, **output_dict},
+        input_dict.keys(),
+        cfg.MODEL.output_keys,
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="laplace2d.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

使用训练得到的模型对上述计算域中均匀取的共 NPOINT_TOTAL 个点 \((x_i,y_i)\) 进行预测,预测结果如下所示。图像中每个点 \((x_i,y_i)\) 的值代表对应坐标上模型对 2D-Laplace 问题预测的解 \(u(x_i,y_i)\)

+
+

laplace 2d +

+
模型预测结果
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/ldc2d_steady/index.html b/zh/examples/ldc2d_steady/index.html new file mode 100644 index 0000000000..d712898772 --- /dev/null +++ b/zh/examples/ldc2d_steady/index.html @@ -0,0 +1,4891 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDC2D_steady - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-LDC(2D Lid Driven Cavity Flow)

+

AI Studio快速体验

+
+
+
+
+
+
+
# linux
+wget -nc -P ./data/ \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re100.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re400.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re100.mat --create-dirs -o ./data/ldc_Re100.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re400.mat --create-dirs -o ./data/ldc_Re400.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat --create-dirs -o ./data/ldc_Re1000.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat --create-dirs -o ./data/ldc_Re3200.mat
+python ldc_2d_Re3200_sota.py
+
+
+
+
# linux
+wget -nc -P ./data/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat --create-dirs -o ./data/ldc_Re1000.mat
+python ldc_2d_Re3200_sota.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/ldc/ldc_re1000_sota_pretrained.pdparams
+
+
+
+
python ldc_2d_Re3200_sota.py mode=export
+
+
+
+
# linux
+wget -nc -P ./data/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat --create-dirs -o ./data/ldc_Re1000.mat
+python ldc_2d_Re3200_sota.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
预训练模型\(Re\)指标
-100U_validator/loss: 0.00017
U_validator/L2Rel.U: 0.04875
-400U_validator/loss: 0.00047
U_validator/L2Rel.U: 0.07554
ldc_re1000_sota_pretrained.pdparams1000U_validator/loss: 0.00053
U_validator/L2Rel.U: 0.07777
-3200U_validator/loss: 0.00227
U_validator/L2Rel.U: 0.15440
+
+
+
+
+
+
# linux
+wget -nc -P ./data/ \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re100.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re400.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1600.mat \
+    https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re100.mat --create-dirs -o ./data/ldc_Re100.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re400.mat --create-dirs -o ./data/ldc_Re400.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1000.mat --create-dirs -o ./data/ldc_Re1000.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re1600.mat --create-dirs -o ./data/ldc_Re1600.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat --create-dirs -o ./data/ldc_Re3200.mat
+python ldc_2d_Re3200_piratenet.py
+
+
+
+
# linux
+wget -nc -P ./data/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat --create-dirs -o ./data/ldc_Re3200.mat
+python ldc_2d_Re3200_piratenet.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/ldc/ldc_re3200_piratenet_pretrained.pdparams
+
+
+
+
python ldc_2d_Re3200_piratenet.py mode=export
+
+
+
+
# linux
+wget -nc -P ./data/ https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/ldc/ldc_Re3200.mat --create-dirs -o ./data/ldc_Re3200.mat
+python ldc_2d_Re3200_piratenet.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
预训练模型\(Re\)指标
-100U_validator/loss: 0.00016
U_validator/L2Rel.U: 0.04741
-400U_validator/loss: 0.00071
U_validator/L2Rel.U: 0.09288
-1000U_validator/loss: 0.00191
U_validator/L2Rel.U: 0.14797
-1600U_validator/loss: 0.00276
U_validator/L2Rel.U: 0.17360
ldc_re3200_piratenet_pretrained.pdparams3200U_validator/loss: 0.00016
U_validator/L2Rel.U: 0.04166
+
+
+
+
+

说明

+

本案例仅提供 \(Re=1000/3200\) 两种情况下的预训练模型,若需要其他雷诺数下的预训练模型,请执行训练命令手动训练即可得到各雷诺数下的模型权重。

+
+

1. 背景简介

+

顶盖方腔驱动流LDC问题在许多领域中都有应用。例如,这个问题可以用于计算流体力学(CFD)领域中验证计算方法的有效性。虽然这个问题的边界条件相对简单,但是其流动特性却非常复杂。在顶盖驱动流LDC中,顶壁朝x方向以U=1的速度移动,而其他三个壁则被定义为无滑移边界条件,即速度为零。

+

此外,顶盖方腔驱动流LDC问题也被用于研究和预测空气动力学中的流动现象。例如,在汽车工业中,通过模拟和分析车体内部的空气流动,可以帮助优化车辆的设计和性能。

+

总的来说,顶盖方腔驱动流LDC问题在计算流体力学、空气动力学以及相关领域中都有广泛的应用,对于研究和预测流动现象、优化产品设计等方面都起到了重要的作用。

+

2. 问题定义

+

本案例假设 \(Re=3200\),计算域为一个长宽均为 1 的方腔,应用以下公式进行顶盖驱动方腔流研究稳态流场问题:

+

质量守恒:

+
\[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ + u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} + \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} + \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) +\]
+

令:

+

\(t^* = \dfrac{L}{U_0}\)

+

\(x^*=y^* = L\)

+

\(u^*=v^* = U_0\)

+

\(p^* = \rho {U_0}^2\)

+

定义:

+

无量纲坐标 \(x:X = \dfrac{x}{x^*}\);无量纲坐标 \(y:Y = \dfrac{y}{y^*}\)

+

无量纲速度 \(x:U = \dfrac{u}{u^*}\);无量纲速度 \(y:V = \dfrac{v}{u^*}\)

+

无量纲压力 \(P = \dfrac{p}{p^*}\)

+

雷诺数 \(Re = \dfrac{L U_0}{\nu}\)

+

则可获得如下无量纲Navier-Stokes方程,施加于方腔内部:

+

质量守恒:

+
\[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} = -\dfrac{\partial P}{\partial X} + \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) +\]
+

\(y\) 动量守恒:

+
\[ +U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} = -\dfrac{\partial P}{\partial Y} + \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) +\]
+

对于方腔边界,则需施加 Dirichlet 边界条件:

+

上边界:

+
\[ +u(x, y) = 1 − \dfrac{\cosh (C_0(x − 0.5))} {\cosh (0.5C_0)} , +\]
+

左边界、下边界、右边界:

+
\[ +u=0, v=0 +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 2D-LDC 问题中,每一个已知的坐标点 \((x, y)\) 都有自身的横向速度 \(u\)、纵向速度 \(v\)、压力 \(p\) +三个待求解的未知量,我们在这里使用适合于 PINN 任务的 PirateNet 来表示 \((x, y)\)\((u, v, p)\) 的映射函数 \(f: \mathbb{R}^2 \to \mathbb{R}^3\) ,即:

+
\[ +u, v, p = f(x, y) +\]
+

上式中 \(f\) 即为 PirateNet 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.PirateNet(**cfg.MODEL)
+
+

其中 cfg.MODEL 配置如下所示:

+
38
+39
+40
+41
# model settings
+MODEL:
+  input_keys: ["x", "y"]
+  output_keys: ["u", "v", "p"]
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ["x", "y"],输出变量名是 ["u", "v", "p"],这些命名与后续代码保持一致。

+

如上所示,通过指定 PirateNet 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 12 层隐藏神经元,每层神经元数为 256,使用 "tanh" 作为激活函数的神经网络模型 model

+

3.2 Curriculum Learning

+

为了加快收敛速度,我们使用 Curriculum learning 的方法来训练模型,即先训练模型在低雷诺数下,然后逐步增加雷诺数,最终达到高雷诺数下的收敛。

+
for idx in range(len(cfg.Re)):
+    train_curriculum(cfg, idx)
+
+

3.3 方程构建

+

由于 2D-LDC 使用的是 Navier-Stokes 方程的2维稳态形式,因此可以直接使用 PaddleScience 内置的 NavierStokes

+
88
+89
+90
+91
# set equation
+equation = {
+    "NavierStokes": ppsci.equation.NavierStokes(1 / Re, 1, dim=2, time=False)
+}
+
+

在课程学习的函数中,我们在实例化 NavierStokes 类时需指定必要的参数:动力粘度 \(\nu=\frac{1}{Re}\), 流体密度 \(\rho=1.0\),其中 \(Re\) 是一个随着训练过程中会逐步增大的变量。

+

3.4 计算域构建

+

本文中 2D-LDC 问题训练、评估所需的数据,通过读取对应雷诺数的文件得到。

+
# load data
+data = sio.loadmat(f"./data/ldc_Re{Re}.mat")
+u_ref = data["u"].astype(dtype)
+v_ref = data["v"].astype(dtype)
+U_ref = np.sqrt(u_ref**2 + v_ref**2).reshape(-1, 1)
+x_star = data["x"].flatten().astype(dtype)
+y_star = data["y"].flatten().astype(dtype)
+x0 = x_star[0]
+x1 = x_star[-1]
+y0 = y_star[0]
+y1 = y_star[-1]
+
+

3.5 约束构建

+

根据 2. 问题定义 得到的无量纲公式和和边界条件,对应了在计算域中指导模型训练的两个约束条件,即:

+
    +
  1. +

    施加在矩形内部点上的无量纲 Navier-Stokes 方程约束(经过简单移项)

    +
    \[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
    +
    \[ +U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} + \dfrac{\partial P}{\partial X} - \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) = 0 +\]
    +
    \[ +U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} + \dfrac{\partial P}{\partial Y} - \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) = 0 +\]
    +

    为了方便获取中间变量,NavierStokes 类内部将上式左侧的结果分别命名为 continuity, momentum_x, momentum_y

    +
  2. +
  3. +

    施加在矩形上、下、左、右边界上的 Dirichlet 边界条件约束

    +

    上边界:

    +
    \[ +u(x, y) = 1 − \dfrac{\cosh (C_0(x − 0.5))} {\cosh (0.5C_0)} , +\]
    +

    左边界、下边界、右边界:

    +
    \[ +u=0, v=0 +\]
    +
  4. +
+

接下来使用 PaddleScience 内置的 SupervisedConstraint 构建上述两种约束条件。

+

3.5.1 内部点约束

+

以作用在矩形内部点上的 SupervisedConstraint 为例,代码如下:

+
# set N-S pde constraint
+def gen_input_batch():
+    tx = np.random.uniform(
+        [x0, y0],
+        [x1, y1],
+        (cfg_t.TRAIN.batch_size.pde, 2),
+    ).astype(dtype)
+    return {"x": tx[:, 0:1], "y": tx[:, 1:2]}
+
+def gen_label_batch(input_batch):
+    return {
+        "continuity": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+        "momentum_x": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+        "momentum_y": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+    }
+
+pde_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "ContinuousNamedArrayDataset",
+            "input": gen_input_batch,
+            "label": gen_label_batch,
+        },
+    },
+    output_expr=equation["NavierStokes"].equations,
+    loss=ppsci.loss.MSELoss("mean"),
+    name="PDE",
+)
+
+

SupervisedConstraint 的第一个参数是数据集配置,用于描述如何构建输入数据,此处填入输入数据和标签数据的构造函数函数 gen_input_batchgen_label_batch,以及数据集的名称 ContinuousNamedArrayDataset

+

第二个参数是约束变量的目标值,此处填入在 3.3 方程构建 章节中实例化好的 equation["NavierStokes"].equations

+

第三个参数是损失函数,此处我们选用常用的 MSE 函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项平均;

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "PDE" 即可。

+

3.5.2 边界约束

+

将上边界的标签数据按照上述对应公式进行处理,其余点的标签数据设置为 0。然后继续构建方腔边界的 Dirichlet 约束,我们仍然使用 SupervisedConstraint 类。

+
# set boundary conditions
+x_bc = sample_points_on_square_boundary(
+    cfg_t.TRAIN.batch_size.bc, eps=0.0
+).astype(
+    dtype
+)  # avoid singularity a right corner for u velocity
+v_bc = np.zeros((cfg_t.TRAIN.batch_size.bc * 4, 1), dtype)
+u_bc = copy.deepcopy(v_bc)
+lid_bc_fn = lambda x: 1 - np.cosh(50 * (x - 0.5)) / np.cosh(50 * 0.5)
+u_bc[: cfg_t.TRAIN.batch_size.bc] = lid_bc_fn(
+    x_bc[: cfg_t.TRAIN.batch_size.bc, 0:1]
+)
+bc = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "input": {
+                "x": x_bc[:, 0:1],
+                "y": x_bc[:, 1:2],
+            },
+            "label": {"u": u_bc, "v": v_bc},
+        },
+    },
+    output_expr={"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    loss=ppsci.loss.MSELoss("mean"),
+    name="BC",
+)
+
+

在微分方程约束、边界约束、初值约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    pde_constraint.name: pde_constraint,
+    bc.name: bc,
+}
+
+

3.6 超参数设定

+

接下来需要在配置文件中指定训练轮数,分别在 Re=100, 400, 1000, 1600, 3200 上训练 10, 20, 50, 50, 500 轮,每轮迭代次数为 1000,

+
33
+34
+35
# working conditions
+Re: [100, 400, 1000, 1600, 3200]
+epochs: [10, 20, 50, 50, 500]
+
+

其次,设置合适的学习率衰减策略,

+
52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
# training settings
+TRAIN:
+  epochs: 10
+  iters_per_epoch: 1000
+  save_freq: 100
+  eval_during_train: true
+  eval_freq: 1
+  lr_scheduler:
+    epochs: ${sum:${epochs}}
+    iters_per_epoch: ${TRAIN.iters_per_epoch}
+    learning_rate: 1.0e-3
+    gamma: 0.9
+    decay_steps: 10000
+    warmup_epoch: 5
+    by_epoch: false
+
+

最后,设置训练过程中损失自动平衡策略为 GradNorm

+
72
+73
+74
+75
grad_norm:
+  update_freq: 1000
+  momentum: 0.9
+  init_weights: [10, 1, 1, 100, 100]
+
+

3.7 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
44
+45
+46
+47
+48
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.8 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器。

+
# set validator
+xy_star = misc.cartesian_product(x_star, y_star).astype(dtype)
+eval_data = {"x": xy_star[:, 0:1], "y": xy_star[:, 1:2]}
+eval_label = {"U": U_ref.reshape([-1, 1])}
+U_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": eval_data,
+            "label": eval_label,
+        },
+        "batch_size": cfg_t.EVAL.batch_size,
+    },
+    ppsci.loss.MSELoss("mean"),
+    {"U": lambda out: (out["u"] ** 2 + out["v"] ** 2).sqrt()},
+    metric={"L2Rel": ppsci.metric.L2Rel()},
+    name="U_validator",
+)
+validator = {U_validator.name: U_validator}
+
+

此处计算 \(U=\sqrt{u^2+v^2}\) 的预测误差;

+

评价指标 metric 选择 ppsci.metric.L2Rel 即可;

+

其余配置与 约束构建 的设置类似。

+

3.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    optimizer=optimizer,
+    equation=equation,
+    validator=validator,
+    loss_aggregator=grad_norm,
+    cfg=cfg_t,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+pred_dict = solver.predict(
+    eval_data, batch_size=cfg_t.EVAL.batch_size, return_numpy=True
+)
+U_pred = np.sqrt(pred_dict["u"] ** 2 + pred_dict["v"] ** 2).reshape(
+    [len(x_star), len(y_star)]
+)
+plot(U_pred, cfg_t.output_dir)
+
+

4. 完整代码

+
ldc_2d_Re3200_piratenet.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
"""
+Reference: https://github.com/PredictiveIntelligenceLab/jaxpi/tree/main/examples/ldc
+"""
+
+from __future__ import annotations
+
+import copy
+import os
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+import scipy.io as sio
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.loss import mtl
+from ppsci.utils import misc
+
+dtype = paddle.get_default_dtype()
+
+
+def plot(U_pred: np.ndarray, output_dir: str):
+    os.makedirs(output_dir, exist_ok=True)
+    fig_path = osp.join(output_dir, "ac.png")
+
+    fig = plt.figure()
+    plt.pcolor(U_pred.T, cmap="jet")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    plt.colorbar()
+    plt.title(r"Prediction of $U=\sqrt{{u^2+v^2}}$")
+    fig.savefig(fig_path, bbox_inches="tight")
+    ppsci.utils.logger.info(f"Saving figure to {fig_path}")
+    plt.close()
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.PirateNet(**cfg.MODEL)
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+        **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+    grad_norm = mtl.GradNorm(
+        model,
+        5,
+        update_freq=cfg.TRAIN.grad_norm.update_freq,
+        momentum=cfg.TRAIN.grad_norm.momentum,
+        init_weights=list(cfg.TRAIN.grad_norm.init_weights),
+    )
+
+    def sample_points_on_square_boundary(num_pts_per_side, eps):
+        # Sample points along the top side (x=1 to x=0, y=1)
+        top_coords = np.linspace(0, 1, num_pts_per_side)
+        top = np.column_stack((top_coords, np.ones_like(top_coords)))
+
+        # Sample points along the bottom side (x=0 to x=1, y=0)
+        bottom_coords = np.linspace(0, 1, num_pts_per_side)
+        bottom = np.column_stack((bottom_coords, np.zeros_like(bottom_coords)))
+
+        # Sample points along the left side (x=0, y=1 to y=0)
+        left_coords = np.linspace(0, 1 - eps, num_pts_per_side)
+        left = np.column_stack((np.zeros_like(left_coords), left_coords))
+
+        # Sample points along the right side (x=1, y=0 to y=1)
+        right_coords = np.linspace(0, 1 - eps, num_pts_per_side)
+        right = np.column_stack((np.ones_like(right_coords), right_coords))
+
+        # Combine the points from all sides
+        points = np.vstack((top, bottom, left, right))
+
+        return points
+
+    def train_curriculum(cfg, idx):
+        cfg_t = copy.deepcopy(cfg)
+        Re = cfg_t.Re[idx]
+        cfg_t.output_dir = osp.join(cfg_t.output_dir, f"Re_{int(Re)}")
+        cfg_t.TRAIN.epochs = cfg_t.epochs[idx]
+        ppsci.utils.logger.message(
+            f"Training curriculum {idx + 1}/{len(cfg_t.epochs)} Re={Re:.5g} epochs={cfg_t.epochs[idx]}"
+        )
+
+        # set equation
+        equation = {
+            "NavierStokes": ppsci.equation.NavierStokes(1 / Re, 1, dim=2, time=False)
+        }
+
+        # load data
+        data = sio.loadmat(f"./data/ldc_Re{Re}.mat")
+        u_ref = data["u"].astype(dtype)
+        v_ref = data["v"].astype(dtype)
+        U_ref = np.sqrt(u_ref**2 + v_ref**2).reshape(-1, 1)
+        x_star = data["x"].flatten().astype(dtype)
+        y_star = data["y"].flatten().astype(dtype)
+        x0 = x_star[0]
+        x1 = x_star[-1]
+        y0 = y_star[0]
+        y1 = y_star[-1]
+
+        # set N-S pde constraint
+        def gen_input_batch():
+            tx = np.random.uniform(
+                [x0, y0],
+                [x1, y1],
+                (cfg_t.TRAIN.batch_size.pde, 2),
+            ).astype(dtype)
+            return {"x": tx[:, 0:1], "y": tx[:, 1:2]}
+
+        def gen_label_batch(input_batch):
+            return {
+                "continuity": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+                "momentum_x": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+                "momentum_y": np.zeros([cfg_t.TRAIN.batch_size.pde, 1], dtype),
+            }
+
+        pde_constraint = ppsci.constraint.SupervisedConstraint(
+            {
+                "dataset": {
+                    "name": "ContinuousNamedArrayDataset",
+                    "input": gen_input_batch,
+                    "label": gen_label_batch,
+                },
+            },
+            output_expr=equation["NavierStokes"].equations,
+            loss=ppsci.loss.MSELoss("mean"),
+            name="PDE",
+        )
+
+        # set boundary conditions
+        x_bc = sample_points_on_square_boundary(
+            cfg_t.TRAIN.batch_size.bc, eps=0.0
+        ).astype(
+            dtype
+        )  # avoid singularity a right corner for u velocity
+        v_bc = np.zeros((cfg_t.TRAIN.batch_size.bc * 4, 1), dtype)
+        u_bc = copy.deepcopy(v_bc)
+        lid_bc_fn = lambda x: 1 - np.cosh(50 * (x - 0.5)) / np.cosh(50 * 0.5)
+        u_bc[: cfg_t.TRAIN.batch_size.bc] = lid_bc_fn(
+            x_bc[: cfg_t.TRAIN.batch_size.bc, 0:1]
+        )
+        bc = ppsci.constraint.SupervisedConstraint(
+            {
+                "dataset": {
+                    "name": "IterableNamedArrayDataset",
+                    "input": {
+                        "x": x_bc[:, 0:1],
+                        "y": x_bc[:, 1:2],
+                    },
+                    "label": {"u": u_bc, "v": v_bc},
+                },
+            },
+            output_expr={"u": lambda out: out["u"], "v": lambda out: out["v"]},
+            loss=ppsci.loss.MSELoss("mean"),
+            name="BC",
+        )
+        # wrap constraints together
+        constraint = {
+            pde_constraint.name: pde_constraint,
+            bc.name: bc,
+        }
+
+        # set validator
+        xy_star = misc.cartesian_product(x_star, y_star).astype(dtype)
+        eval_data = {"x": xy_star[:, 0:1], "y": xy_star[:, 1:2]}
+        eval_label = {"U": U_ref.reshape([-1, 1])}
+        U_validator = ppsci.validate.SupervisedValidator(
+            {
+                "dataset": {
+                    "name": "NamedArrayDataset",
+                    "input": eval_data,
+                    "label": eval_label,
+                },
+                "batch_size": cfg_t.EVAL.batch_size,
+            },
+            ppsci.loss.MSELoss("mean"),
+            {"U": lambda out: (out["u"] ** 2 + out["v"] ** 2).sqrt()},
+            metric={"L2Rel": ppsci.metric.L2Rel()},
+            name="U_validator",
+        )
+        validator = {U_validator.name: U_validator}
+
+        # initialize solver
+        solver = ppsci.solver.Solver(
+            model,
+            constraint,
+            optimizer=optimizer,
+            equation=equation,
+            validator=validator,
+            loss_aggregator=grad_norm,
+            cfg=cfg_t,
+        )
+        # train model
+        solver.train()
+        # evaluate after finished training
+        solver.eval()
+        # visualize prediction after finished training
+        pred_dict = solver.predict(
+            eval_data, batch_size=cfg_t.EVAL.batch_size, return_numpy=True
+        )
+        U_pred = np.sqrt(pred_dict["u"] ** 2 + pred_dict["v"] ** 2).reshape(
+            [len(x_star), len(y_star)]
+        )
+        plot(U_pred, cfg_t.output_dir)
+
+    for idx in range(len(cfg.Re)):
+        train_curriculum(cfg, idx)
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.PirateNet(**cfg.MODEL)
+
+    data = sio.loadmat(cfg.EVAL_DATA_PATH)
+    data = dict(data)
+    u_ref = data["u"].astype(dtype)
+    v_ref = data["v"].astype(dtype)
+    U_ref = np.sqrt(u_ref**2 + v_ref**2).reshape(-1, 1)
+    x_star = data["x"].flatten().astype(dtype)  # [nx, ]
+    y_star = data["y"].flatten().astype(dtype)  # [ny, ]
+
+    # set validator
+    xy_star = misc.cartesian_product(x_star, y_star).astype(dtype)
+    eval_data = {"x": xy_star[:, 0:1], "y": xy_star[:, 1:2]}
+    eval_label = {"U": U_ref.reshape([-1, 1])}
+    U_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": eval_data,
+                "label": eval_label,
+            },
+            "batch_size": cfg.EVAL.batch_size,
+        },
+        ppsci.loss.MSELoss("mean"),
+        {"U": lambda out: (out["u"] ** 2 + out["v"] ** 2).sqrt()},
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="U_validator",
+    )
+    validator = {U_validator.name: U_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        validator=validator,
+        cfg=cfg,
+    )
+
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    pred_dict = solver.predict(
+        eval_data, batch_size=cfg.EVAL.batch_size, return_numpy=True
+    )
+    U_pred = np.sqrt(pred_dict["u"] ** 2 + pred_dict["v"] ** 2).reshape(
+        [len(x_star), len(y_star)]
+    )
+    # plot
+    plot(U_pred, cfg.output_dir)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.PirateNet(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(model, cfg=cfg)
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path, with_onnx=False)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+    data = sio.loadmat(cfg.EVAL_DATA_PATH)
+    data = dict(data)
+    x_star = data["x"].flatten().astype(dtype)  # [nx, ]
+    y_star = data["y"].flatten().astype(dtype)  # [ny, ]
+    xy_star = misc.cartesian_product(x_star, y_star).astype(dtype)
+    input_dict = {"x": xy_star[:, 0:1], "y": xy_star[:, 1:2]}
+
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+    U_pred = np.sqrt(output_dict["u"] ** 2 + output_dict["v"] ** 2).reshape(
+        [len(x_star), len(y_star)]
+    )
+    plot(U_pred, cfg.output_dir)
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="ldc_2d_Re3200_piratenet.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下方展示了模型对于边长为 1 的正方形计算域的内部点进行预测的结果 \(U=\sqrt{u^2+v^2}\)

+
+
+
+

+ldc_re1000_sota_ac +
+

+

可以看到在 \(Re=1000\) 下,预测结果与求解器的结果基本相同(L2 相对误差为 7.7%)。

+
+
+

+ldc_re3200_piratenet_ac +
+

+

可以看到在 \(Re=3200\) 下,预测结果与求解器的结果基本相同(L2 相对误差为 4.1%)。

+
+
+
+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/ldc2d_unsteady/index.html b/zh/examples/ldc2d_unsteady/index.html new file mode 100644 index 0000000000..cfee57d3fd --- /dev/null +++ b/zh/examples/ldc2d_unsteady/index.html @@ -0,0 +1,5008 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LDC2D_unsteady - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

2D-LDC(2D Lid Driven Cavity Flow)

+

AI Studio快速体验

+
+
+
+
python ldc2d_unsteady_Re10.py
+
+
+
+
python ldc2d_unsteady_Re10.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/ldc2d_unsteady_Re10/ldc2d_unsteady_Re10_pretrained.pdparams
+
+
+
+
python ldc2d_unsteady_Re10.py mode=export
+
+
+
+
python ldc2d_unsteady_Re10.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
ldc2d_unsteady_Re10_pretrained.pdparamsloss(Residual): 155652.67530
MSE.momentum_x(Residual): 6.78030
MSE.continuity(Residual): 0.16590
MSE.momentum_y(Residual): 12.05981
+

1. 背景简介

+

顶盖方腔驱动流LDC问题在许多领域中都有应用。例如,这个问题可以用于计算流体力学(CFD)领域中验证计算方法的有效性。虽然这个问题的边界条件相对简单,但是其流动特性却非常复杂。在顶盖驱动流LDC中,顶壁朝x方向以U=1的速度移动,而其他三个壁则被定义为无滑移边界条件,即速度为零。

+

此外,顶盖方腔驱动流LDC问题也被用于研究和预测空气动力学中的流动现象。例如,在汽车工业中,通过模拟和分析车体内部的空气流动,可以帮助优化车辆的设计和性能。

+

总的来说,顶盖方腔驱动流LDC问题在计算流体力学、空气动力学以及相关领域中都有广泛的应用,对于研究和预测流动现象、优化产品设计等方面都起到了重要的作用。

+

2. 问题定义

+

本案例中我们对于 16 个时刻内长宽均为 1 的方腔内部作为计算域,并应用以下公式进行顶盖驱动方腔流研究瞬态流场问题:

+

质量守恒:

+
\[ +\dfrac{\partial u}{\partial x} + \dfrac{\partial v}{\partial y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\dfrac{\partial u}{\partial t} + u\dfrac{\partial u}{\partial x} + v\dfrac{\partial u}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial x} + \nu(\dfrac{\partial ^2 u}{\partial x ^2} + \dfrac{\partial ^2 u}{\partial y ^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\dfrac{\partial v}{\partial t} + u\dfrac{\partial v}{\partial x} + v\dfrac{\partial v}{\partial y} = -\dfrac{1}{\rho}\dfrac{\partial p}{\partial y} + \nu(\dfrac{\partial ^2 v}{\partial x ^2} + \dfrac{\partial ^2 v}{\partial y ^2}) +\]
+

令:

+

\(t^* = \dfrac{L}{U_0}\)

+

\(x^*=y^* = L\)

+

\(u^*=v^* = U_0\)

+

\(p^* = \rho {U_0}^2\)

+

定义:

+

无量纲时间 \(\tau = \dfrac{t}{t^*}\)

+

无量纲坐标 \(x:X = \dfrac{x}{x^*}\);无量纲坐标 \(y:Y = \dfrac{y}{y^*}\)

+

无量纲速度 \(x:U = \dfrac{u}{u^*}\);无量纲速度 \(y:V = \dfrac{v}{u^*}\)

+

无量纲压力 \(P = \dfrac{p}{p^*}\)

+

雷诺数 \(Re = \dfrac{L U_0}{\nu}\)

+

则可获得如下无量纲Navier-Stokes方程,施加于方腔内部:

+

质量守恒:

+
\[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
+

\(x\) 动量守恒:

+
\[ +\dfrac{\partial U}{\partial \tau} + U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} = -\dfrac{\partial P}{\partial X} + \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) +\]
+

\(y\) 动量守恒:

+
\[ +\dfrac{\partial V}{\partial \tau} + U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} = -\dfrac{\partial P}{\partial Y} + \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) +\]
+

对于方腔边界,则需施加 Dirichlet 边界条件:

+

上边界:

+
\[ +u=1, v=0 +\]
+

下边界

+
\[ +u=0, v=0 +\]
+

左边界:

+
\[ +u=0, v=0 +\]
+

右边界:

+
\[ +u=0, v=0 +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 2D-LDC 问题中,每一个已知的坐标点 \((t, x, y)\) 都有自身的横向速度 \(u\)、纵向速度 \(v\)、压力 \(p\) +三个待求解的未知量,我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((t, x, y)\)\((u, v, p)\) 的映射函数 \(f: \mathbb{R}^3 \to \mathbb{R}^3\) ,即:

+
\[ +u, v, p = f(t, x, y) +\]
+

上式中 \(f\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ["t", "x", "y"],输出变量名是 ["u", "v", "p"],这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 9 层隐藏神经元,每层神经元数为 50,使用 "tanh" 作为激活函数的神经网络模型 model

+

3.2 方程构建

+

由于 2D-LDC 使用的是 Navier-Stokes 方程的2维瞬态形式,因此可以直接使用 PaddleScience 内置的 NavierStokes

+
# set equation
+equation = {"NavierStokes": ppsci.equation.NavierStokes(cfg.NU, cfg.RHO, 2, True)}
+
+

在实例化 NavierStokes 类时需指定必要的参数:动力粘度 \(\nu=0.01\), 流体密度 \(\rho=1.0\)

+

3.3 计算域构建

+

本文中 2D-LDC 问题作用在以 [-0.05, -0.05], [0.05, 0.05] 为对角线的二维矩形区域,且时间域为 16 个时刻 [0.0, 0.1, ..., 1.4, 1.5], +因此可以直接使用 PaddleScience 内置的空间几何 Rectangle 和时间域 TimeDomain,组合成时间-空间的 TimeXGeometry 计算域。

+
36
+37
+38
+39
+40
+41
+42
+43
+44
# set timestamps(including initial t0)
+timestamps = np.linspace(0.0, 1.5, cfg.NTIME_ALL, endpoint=True)
+# set time-geometry
+geom = {
+    "time_rect": ppsci.geometry.TimeXGeometry(
+        ppsci.geometry.TimeDomain(0.0, 1.5, timestamps=timestamps),
+        ppsci.geometry.Rectangle((-0.05, -0.05), (0.05, 0.05)),
+    )
+}
+
+
+提示 +

RectangleTimeDomain 是两种可以单独使用的 Geometry 派生类。

+

如输入数据只来自于二维矩形几何域,则可以直接使用 ppsci.geometry.Rectangle(...) 创建空间几何域对象;

+

如输入数据只来自一维时间域,则可以直接使用 ppsci.geometry.TimeDomain(...) 构建时间域对象。

+
+

3.4 约束构建

+

根据 2. 问题定义 得到的无量纲公式和和边界条件,对应了在计算域中指导模型训练的两个约束条件,即:

+
    +
  1. +

    施加在矩形内部点上的无量纲 Navier-Stokes 方程约束(经过简单移项)

    +
    \[ +\dfrac{\partial U}{\partial X} + \dfrac{\partial U}{\partial Y} = 0 +\]
    +
    \[ +\dfrac{\partial U}{\partial \tau} + U\dfrac{\partial U}{\partial X} + V\dfrac{\partial U}{\partial Y} + \dfrac{\partial P}{\partial X} - \dfrac{1}{Re}(\dfrac{\partial ^2 U}{\partial X^2} + \dfrac{\partial ^2 U}{\partial Y^2}) = 0 +\]
    +
    \[ +\dfrac{\partial V}{\partial \tau} + U\dfrac{\partial V}{\partial X} + V\dfrac{\partial V}{\partial Y} + \dfrac{\partial P}{\partial Y} - \dfrac{1}{Re}(\dfrac{\partial ^2 V}{\partial X^2} + \dfrac{\partial ^2 V}{\partial Y^2}) = 0 +\]
    +

    为了方便获取中间变量,NavierStokes 类内部将上式左侧的结果分别命名为 continuity, momentum_x, momentum_y

    +
  2. +
  3. +

    施加在矩形上、下、左、右边界上的 Dirichlet 边界条件约束

    +
    \[ +上边界:u=1,v=0 +\]
    +
    \[ +下边界:u=0,v=0 +\]
    +
    \[ +左边界:u=0,v=0 +\]
    +
    \[ +右边界:u=0,v=0 +\]
    +
  4. +
+

接下来使用 PaddleScience 内置的 InteriorConstraintBoundaryConstraint 构建上述两种约束条件。

+

在定义约束之前,需要给每一种约束指定采样点个数,这表示某种约束在其对应计算域内采样数据的数量,以及指定通用的采样配置。

+
46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
# set dataloader config
+train_dataloader_cfg = {
+    "dataset": "IterableNamedArrayDataset",
+    "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+}
+
+# pde/bc constraint use t1~tn, initial constraint use t0
+NPOINT_PDE, NTIME_PDE = 99**2, cfg.NTIME_ALL - 1
+NPOINT_TOP, NTIME_TOP = 101, cfg.NTIME_ALL - 1
+NPOINT_DOWN, NTIME_DOWN = 101, cfg.NTIME_ALL - 1
+NPOINT_LEFT, NTIME_LEFT = 99, cfg.NTIME_ALL - 1
+NPOINT_RIGHT, NTIME_RIGHT = 99, cfg.NTIME_ALL - 1
+NPOINT_IC, NTIME_IC = 99**2, 1
+
+

3.4.1 内部点约束

+

以作用在矩形内部点上的 InteriorConstraint 为例,代码如下:

+
60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
# set constraint
+pde = ppsci.constraint.InteriorConstraint(
+    equation["NavierStokes"].equations,
+    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_PDE * NTIME_PDE},
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    weight_dict=cfg.TRAIN.weight.pde,  # (1)
+    name="EQ",
+)
+
+
    +
  1. 本案例中PDE约束损失的数量级远大于边界约束损失,因此需要给PDE约束权重设置一个较小的值,有利于模型收敛
  2. +
+

InteriorConstraint 的第一个参数是方程表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["NavierStokes"].equations

+

第二个参数是约束变量的目标值,在本问题中我们希望 Navier-Stokes 方程产生的三个中间结果 continuity, momentum_x, momentum_y 被优化至 0,因此将它们的目标值全部设为 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["time_rect"] 即可;

+

第四个参数是在计算域上的采样配置,此处我们使用全量数据点训练,因此 dataset 字段设置为 "IterableNamedArrayDataset" 且 iters_per_epoch 也设置为 1,采样点数 batch_size 设为 9801 * 15(表示99x99的等间隔网格,共有 15 个时刻的网格);

+

第五个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "sum",即我们会将参与计算的所有数据点产生的损失项求和;

+

第六个参数是选择是否在计算域上进行等间隔采样,此处我们选择开启等间隔采样,这样能让训练点均匀分布在计算域上,有利于训练收敛;

+

第七个参数是权重系数,该配置可以精确调整每一个变量参与损失计算时的权重,设置为 0.0001 是一个比较合适的值;

+

第八个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "EQ" 即可。

+

3.4.2 边界约束

+

同理,我们还需要构建矩形的上、下、左、右四个边界的 Dirichlet 边界约束。但与构建 InteriorConstraint 约束不同的是,由于作用区域是边界,因此我们使用 BoundaryConstraint 类。

+

其次约束的目标变量也不同,Dirichlet 条件约束对象是 MLP 模型输出的 \(u\)\(v\)(本文不对 \(p\) 做约束),因此第一个参数使用 lambda 表达式直接返回 MLP 的输出结果 out["u"]out["v"] 作为程序运行时的约束对象。

+

然后给 \(u\)\(v\) 设置约束目标值,请注意在 bc_top 上边界中,\(u\) 的约束目标值要设置为 1。

+

采样点与损失函数配置和 InteriorConstraint 类似,单个时刻的点数设置为 100 左右。

+

由于 BoundaryConstraint 默认会在所有边界上进行采样,而我们需要对四个边界分别施加约束,因此需通过设置 criteria 参数,进一步细化出四个边界,如上边界就是符合 \(y = 0.05\) 的边界点集

+
bc_top = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    {"u": 1, "v": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_TOP * NTIME_TOP},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda t, x, y: np.isclose(y, 0.05),
+    name="BC_top",
+)
+bc_down = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    {"u": 0, "v": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_DOWN * NTIME_DOWN},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda t, x, y: np.isclose(y, -0.05),
+    name="BC_down",
+)
+bc_left = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    {"u": 0, "v": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_LEFT * NTIME_LEFT},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda t, x, y: np.isclose(x, -0.05),
+    name="BC_left",
+)
+bc_right = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    {"u": 0, "v": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_RIGHT * NTIME_RIGHT},
+    ppsci.loss.MSELoss("sum"),
+    criteria=lambda t, x, y: np.isclose(x, 0.05),
+    name="BC_right",
+)
+
+

3.4.3 初值约束

+

最后我们还需要对 \(t=t_0\) 时刻的矩形内部点施加 N-S 方程约束,代码如下:

+
ic = ppsci.constraint.InitialConstraint(
+    {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+    {"u": 0, "v": 0},
+    geom["time_rect"],
+    {**train_dataloader_cfg, "batch_size": NPOINT_IC * NTIME_IC},
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    name="IC",
+)
+
+

在微分方程约束、边界约束、初值约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    pde.name: pde,
+    bc_top.name: bc_top,
+    bc_down.name: bc_down,
+    bc_left.name: bc_left,
+    bc_right.name: bc_right,
+    ic.name: ic,
+}
+
+

3.5 超参数设定

+

接下来需要在配置文件中指定训练轮数,此处我们按实验经验,使用两万轮训练轮数和带有 warmup 的 Cosine 余弦衰减学习率。

+
40
+41
+42
+43
# training settings
+TRAIN:
+  epochs: 20000
+  iters_per_epoch: 1
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
# set validator
+NPOINT_EVAL = NPOINT_PDE * cfg.NTIME_ALL
+residual_validator = ppsci.validate.GeometryValidator(
+    equation["NavierStokes"].equations,
+    {"momentum_x": 0, "continuity": 0, "momentum_y": 0},
+    geom["time_rect"],
+    {
+        "dataset": "NamedArrayDataset",
+        "total_size": NPOINT_EVAL,
+        "batch_size": cfg.EVAL.batch_size.residual_validator,
+        "sampler": {"name": "BatchSampler"},
+    },
+    ppsci.loss.MSELoss("sum"),
+    evenly=True,
+    metric={"MSE": ppsci.metric.MSE()},
+    with_initial=True,
+    name="Residual",
+)
+validator = {residual_validator.name: residual_validator}
+
+

方程设置与 约束构建 的设置相同,表示如何计算所需评估的目标变量;

+

此处我们为 momentum_x, continuity, momentum_y 三个目标变量设置标签值为 0;

+

计算域与 约束构建 的设置相同,表示在指定计算域上进行评估;

+

采样点配置则需要指定总的评估点数 total_size,此处我们设置为 9801 * 16(99x99等间隔网格,共 16 个评估时刻);

+

评价指标 metric 选择 ppsci.metric.MSE 即可;

+

其余配置与 约束构建 的设置类似。

+

3.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文中的输出数据是一个区域内的二维点集,每个时刻 \(t\) 的坐标是 \((x^t_i,y^t_i)\),对应值是 \((u^t_i, v^t_i, p^t_i)\),因此我们只需要将评估的输出数据按时刻保存成 16 个 vtu格式 文件,最后用可视化软件打开查看即可。代码如下:

+
# set visualizer(optional)
+NPOINT_BC = NPOINT_TOP + NPOINT_DOWN + NPOINT_LEFT + NPOINT_RIGHT
+vis_initial_points = geom["time_rect"].sample_initial_interior(
+    (NPOINT_IC + NPOINT_BC), evenly=True
+)
+vis_pde_points = geom["time_rect"].sample_interior(
+    (NPOINT_PDE + NPOINT_BC) * NTIME_PDE, evenly=True
+)
+vis_points = vis_initial_points
+# manually collate input data for visualization,
+# (interior+boundary) x all timestamps
+for t in range(NTIME_PDE):
+    for key in geom["time_rect"].dim_keys:
+        vis_points[key] = np.concatenate(
+            (
+                vis_points[key],
+                vis_pde_points[key][
+                    t
+                    * (NPOINT_PDE + NPOINT_BC) : (t + 1)
+                    * (NPOINT_PDE + NPOINT_BC)
+                ],
+            )
+        )
+
+visualizer = {
+    "visualize_u_v": ppsci.visualize.VisualizerVtu(
+        vis_points,
+        {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+        num_timestamps=cfg.NTIME_ALL,
+        prefix="result_u_v",
+    )
+}
+
+

3.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
ldc2d_steady_Re10.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from os import path as osp
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"NavierStokes": ppsci.equation.NavierStokes(cfg.NU, cfg.RHO, 2, True)}
+
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(0.0, 1.5, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1.5, timestamps=timestamps),
+            ppsci.geometry.Rectangle((-0.05, -0.05), (0.05, 0.05)),
+        )
+    }
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": "IterableNamedArrayDataset",
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+
+    # pde/bc constraint use t1~tn, initial constraint use t0
+    NPOINT_PDE, NTIME_PDE = 99**2, cfg.NTIME_ALL - 1
+    NPOINT_TOP, NTIME_TOP = 101, cfg.NTIME_ALL - 1
+    NPOINT_DOWN, NTIME_DOWN = 101, cfg.NTIME_ALL - 1
+    NPOINT_LEFT, NTIME_LEFT = 99, cfg.NTIME_ALL - 1
+    NPOINT_RIGHT, NTIME_RIGHT = 99, cfg.NTIME_ALL - 1
+    NPOINT_IC, NTIME_IC = 99**2, 1
+
+    # set constraint
+    pde = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_PDE * NTIME_PDE},
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        weight_dict=cfg.TRAIN.weight.pde,  # (1)
+        name="EQ",
+    )
+    bc_top = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+        {"u": 1, "v": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_TOP * NTIME_TOP},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda t, x, y: np.isclose(y, 0.05),
+        name="BC_top",
+    )
+    bc_down = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+        {"u": 0, "v": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_DOWN * NTIME_DOWN},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda t, x, y: np.isclose(y, -0.05),
+        name="BC_down",
+    )
+    bc_left = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+        {"u": 0, "v": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_LEFT * NTIME_LEFT},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda t, x, y: np.isclose(x, -0.05),
+        name="BC_left",
+    )
+    bc_right = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+        {"u": 0, "v": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_RIGHT * NTIME_RIGHT},
+        ppsci.loss.MSELoss("sum"),
+        criteria=lambda t, x, y: np.isclose(x, 0.05),
+        name="BC_right",
+    )
+    ic = ppsci.constraint.InitialConstraint(
+        {"u": lambda out: out["u"], "v": lambda out: out["v"]},
+        {"u": 0, "v": 0},
+        geom["time_rect"],
+        {**train_dataloader_cfg, "batch_size": NPOINT_IC * NTIME_IC},
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        name="IC",
+    )
+    # wrap constraints together
+    constraint = {
+        pde.name: pde,
+        bc_top.name: bc_top,
+        bc_down.name: bc_down,
+        bc_left.name: bc_left,
+        bc_right.name: bc_right,
+        ic.name: ic,
+    }
+
+    # set training hyper-parameters
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Cosine(
+        **cfg.TRAIN.lr_scheduler,
+        warmup_epoch=int(0.05 * cfg.TRAIN.epochs),
+    )()
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    # set validator
+    NPOINT_EVAL = NPOINT_PDE * cfg.NTIME_ALL
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NavierStokes"].equations,
+        {"momentum_x": 0, "continuity": 0, "momentum_y": 0},
+        geom["time_rect"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": NPOINT_EVAL,
+            "batch_size": cfg.EVAL.batch_size.residual_validator,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer(optional)
+    NPOINT_BC = NPOINT_TOP + NPOINT_DOWN + NPOINT_LEFT + NPOINT_RIGHT
+    vis_initial_points = geom["time_rect"].sample_initial_interior(
+        (NPOINT_IC + NPOINT_BC), evenly=True
+    )
+    vis_pde_points = geom["time_rect"].sample_interior(
+        (NPOINT_PDE + NPOINT_BC) * NTIME_PDE, evenly=True
+    )
+    vis_points = vis_initial_points
+    # manually collate input data for visualization,
+    # (interior+boundary) x all timestamps
+    for t in range(NTIME_PDE):
+        for key in geom["time_rect"].dim_keys:
+            vis_points[key] = np.concatenate(
+                (
+                    vis_points[key],
+                    vis_pde_points[key][
+                        t
+                        * (NPOINT_PDE + NPOINT_BC) : (t + 1)
+                        * (NPOINT_PDE + NPOINT_BC)
+                    ],
+                )
+            )
+
+    visualizer = {
+        "visualize_u_v": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+            num_timestamps=cfg.NTIME_ALL,
+            prefix="result_u_v",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"NavierStokes": ppsci.equation.NavierStokes(cfg.NU, cfg.RHO, 2, True)}
+
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(0.0, 1.5, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1.5, timestamps=timestamps),
+            ppsci.geometry.Rectangle((-0.05, -0.05), (0.05, 0.05)),
+        )
+    }
+
+    # pde/bc constraint use t1~tn, initial constraint use t0
+    NPOINT_PDE = 99**2
+    NPOINT_TOP = 101
+    NPOINT_DOWN = 101
+    NPOINT_LEFT = 99
+    NPOINT_RIGHT = 99
+    NPOINT_IC = 99**2
+    NTIME_PDE = cfg.NTIME_ALL - 1
+
+    # set validator
+    NPOINT_EVAL = NPOINT_PDE * cfg.NTIME_ALL
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NavierStokes"].equations,
+        {"momentum_x": 0, "continuity": 0, "momentum_y": 0},
+        geom["time_rect"],
+        {
+            "dataset": "NamedArrayDataset",
+            "total_size": NPOINT_EVAL,
+            "batch_size": cfg.EVAL.batch_size.residual_validator,
+            "sampler": {"name": "BatchSampler"},
+        },
+        ppsci.loss.MSELoss("sum"),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # set visualizer(optional)
+    NPOINT_BC = NPOINT_TOP + NPOINT_DOWN + NPOINT_LEFT + NPOINT_RIGHT
+    vis_initial_points = geom["time_rect"].sample_initial_interior(
+        (NPOINT_IC + NPOINT_BC), evenly=True
+    )
+    vis_pde_points = geom["time_rect"].sample_interior(
+        (NPOINT_PDE + NPOINT_BC) * NTIME_PDE, evenly=True
+    )
+    vis_points = vis_initial_points
+    # manually collate input data for visualization,
+    # (interior+boundary) x all timestamps
+    for t in range(NTIME_PDE):
+        for key in geom["time_rect"].dim_keys:
+            vis_points[key] = np.concatenate(
+                (
+                    vis_points[key],
+                    vis_pde_points[key][
+                        t
+                        * (NPOINT_PDE + NPOINT_BC) : (t + 1)
+                        * (NPOINT_PDE + NPOINT_BC)
+                    ],
+                )
+            )
+
+    visualizer = {
+        "visualize_u_v": ppsci.visualize.VisualizerVtu(
+            vis_points,
+            {"u": lambda d: d["u"], "v": lambda d: d["v"], "p": lambda d: d["p"]},
+            num_timestamps=cfg.NTIME_ALL,
+            prefix="result_u_v",
+        )
+    }
+
+    # directly evaluate pretrained model(optional)
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction for pretrained model(optional)
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(0.0, 1.5, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_rect": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(0.0, 1.5, timestamps=timestamps),
+            ppsci.geometry.Rectangle((-0.05, -0.05), (0.05, 0.05)),
+        )
+    }
+    # manually collate input data for inference
+    NPOINT_PDE = 99**2
+    NPOINT_TOP = 101
+    NPOINT_DOWN = 101
+    NPOINT_LEFT = 99
+    NPOINT_RIGHT = 99
+    NPOINT_IC = 99**2
+    NTIME_PDE = cfg.NTIME_ALL - 1
+    NPOINT_BC = NPOINT_TOP + NPOINT_DOWN + NPOINT_LEFT + NPOINT_RIGHT
+    input_dict = geom["time_rect"].sample_initial_interior(
+        (NPOINT_IC + NPOINT_BC), evenly=True
+    )
+    input_pde_dict = geom["time_rect"].sample_interior(
+        (NPOINT_PDE + NPOINT_BC) * NTIME_PDE, evenly=True
+    )
+    # (interior+boundary) x all timestamps
+    for t in range(NTIME_PDE):
+        for key in geom["time_rect"].dim_keys:
+            input_dict[key] = np.concatenate(
+                (
+                    input_dict[key],
+                    input_pde_dict[key][
+                        t
+                        * (NPOINT_PDE + NPOINT_BC) : (t + 1)
+                        * (NPOINT_PDE + NPOINT_BC)
+                    ],
+                )
+            )
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    ppsci.visualize.save_vtu_from_dict(
+        "./ldc2d_unsteady_Re10_pred.vtu",
+        {**input_dict, **output_dict},
+        input_dict.keys(),
+        cfg.MODEL.output_keys,
+        cfg.NTIME_ALL,
+    )
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="ldc2d_unsteady_Re10.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下方展示了模型对于最后一个时刻,边长为 1 的正方形计算域的内部点进行预测的结果、OpeFOAM求解结果,包括每个点的水平(x)方向流速\(u(x,y)\)、垂直(y)方向流速\(v(x,y)\)、压力\(p(x,y)\)

+
+说明 +

本案例只作为demo展示,尚未进行充分调优,下方部分展示结果可能与 OpenFOAM 存在一定差别。

+
+
+

u_pred_openfoam +

+
左:模型预测结果 u ,右:OpenFOAM结果 u
+
+
+

v_pred_openfoam +

+
左:模型预测结果 v ,右:OpenFOAM结果 v
+
+
+

p_pred_openfoam +

+
左:模型预测结果 p ,右:OpenFOAM结果 p
+
+

可以看到模型预测结果与OpenFOAM的预测结果大致相同。

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/lorenz/index.html b/zh/examples/lorenz/index.html new file mode 100644 index 0000000000..3cbf69d506 --- /dev/null +++ b/zh/examples/lorenz/index.html @@ -0,0 +1,5548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorenz_transform_physx - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Lorenz System

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 --create-dirs -o ./datasets/lorenz_training_rk.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 --create-dirs -o ./datasets/lorenz_valid_rk.hdf5
+python train_enn.py
+python train_transformer.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 --create-dirs -o ./datasets/lorenz_training_rk.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 --create-dirs -o ./datasets/lorenz_valid_rk.hdf5
+python train_enn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/lorenz/lorenz_pretrained.pdparams
+python train_transformer.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/lorenz/lorenz_transformer_pretrained.pdparams EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/lorenz/lorenz_pretrained.pdparams
+
+
+
+
python train_transformer.py mode=export EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/lorenz/lorenz_pretrained.pdparams
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_training_rk.hdf5 --create-dirs -o ./datasets/lorenz_training_rk.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/lorenz_valid_rk.hdf5 --create-dirs -o ./datasets/lorenz_valid_rk.hdf5
+python train_transformer.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
模型MSE
lorenz_transformer_pretrained.pdparams0.054
+

1. 背景简介

+

Lorenz System,中文名称可译作“洛伦兹系统”,又称“洛伦兹混沌系统”,最早由美国气象学家爱德华·洛伦兹(Edward N.Lorenz)在1963年的一篇文章中提出。著名的“蝴蝶效应”,即“一只南美洲亚马逊河流域热带雨林中的蝴蝶,偶尔扇动几下翅膀,可以在两周以后引起美国得克萨斯州的一场龙卷风”,也是最早起源于这篇文章。洛伦兹系统的特点是在一定参数条件下展现出复杂、不确定的动态行为,包括对初始条件的敏感性和长期行为的不可预测性。这种混沌行为在自然界和许多实际应用领域中都存在,例如气候变化、股票市场波动等。洛伦兹系统对数值扰动极为敏感,是评估机器学习(深度学习)模型准确性的良好基准。

+

2. 问题定义

+

洛伦兹系统的状态方程:

+
\[ +\begin{cases} + \dfrac{\partial x}{\partial t} = \sigma(y - x), & \\ + \dfrac{\partial y}{\partial t} = x(\rho - z) - y, & \\ + \dfrac{\partial z}{\partial t} = xy - \beta z +\end{cases} +\]
+

当参数取以下值时,系统表现出经典的混沌特性:

+
\[\rho = 28, \sigma = 10, \beta = \frac{8}{3}\]
+

在这个案例中,要求给定初始时刻点的坐标,预测未来一段时间内点的运动轨迹。

+

3. 问题求解

+

接下来开始讲解如何基于 PaddleScience 代码,用深度学习的方法求解该问题。本案例基于论文 Transformers for Modeling Physical Systems 方法进行求解,接下来首先会对该论文的理论方法进行简单介绍,然后对使用的数据集进行介绍,最后对该方法两个训练步骤(Embedding 模型训练、Transformer 模型训练)的监督约束构建、模型构建等进行阐述,而其余细节请参考 API文档

+

3.1 方法介绍

+

Transformer 结构在 NLP、CV 领域中取得了巨大的成功,但是其在建模物理系统方面还没有得到更多的探索。在 Transformers for Modeling Physical Systems 这篇文章中,作者提出了基于 Transformer 的网络结构用于建模物理系统。实验结果表明,提出的方法能够准确的建模不同的动态系统,并且比其他传统的方法更好。

+

如下图所示,该方法主要包含两个网络模型:Embedding 模型和 Transformer 模型。其中,Embedding 模型的 Encoder 模块负责将物理状态变量进行编码映射为编码向量,Decoder 模块则负责将编码向量映射为物理状态变量;Transformer 模型作用于编码空间,其输入是 Embedding 模型 Encoder 模块的输出,利用当前时刻的编码向量预测下一时刻的编码向量,预测得到的编码向量可以被 Embedding 模型的 Decoder 模块解码,得到对应的物理状态变量。在模型训练时,首先训练 Embedding 模型,然后将 Embedding 模型的参数冻结训练 Transformer 模型。关于该方法的细节请参考论文 Transformers for Modeling Physical Systems

+
+

trphysx-arch +

+
左:Embedding 网络结构,右:Transformer 网络结构
+
+

3.2 数据集介绍

+

数据集采用了 Transformer-Physx 中提供的数据。该数据集使用龙格-库塔(Runge-Kutta)传统数值求解方法得到,每个时间步大小为0.01,初始位置从以下范围中随机选取:

+
\[x_{0} \sim(-20, 20), y_{0} \sim(-20, 20), z_{0} \sim(10, 40)\]
+

数据集的划分如下:

+ + + + + + + + + + + + + + + + + + + + + + + +
数据集时间序列的数量时间步的数量下载地址
训练集2048256lorenz_training_rk.hdf5
验证集641024lorenz_valid_rk.hdf5
+

数据集官网为:https://zenodo.org/record/5148524#.ZDe77-xByrc

+

3.3 Embedding 模型

+

首先展示代码中定义的各个参数变量,每个参数的具体含义会在下面使用到时进行解释。

+
examples/conf/enn.yaml
26
+27
+28
+29
+30
+31
+32
+33
+34
TRAIN_BLOCK_SIZE: 16
+VALID_BLOCK_SIZE: 32
+TRAIN_FILE_PATH: ./datasets/lorenz_training_rk.hdf5
+VALID_FILE_PATH: ./datasets/lorenz_valid_rk.hdf5
+
+# model settings
+MODEL:
+  input_keys: ["states"]
+  output_keys: ["pred_states", "recover_states"]
+
+

3.3.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/lorenz/train_enn.py
51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
train_dataloader_cfg = {
+    "dataset": {
+        "name": "LorenzDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 16,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 LorenzDataset,另外还指定了该类初始化时参数的取值:

+
    +
  1. file_path:代表训练数据集的文件路径,指定为变量 train_file_path 的值;
  2. +
  3. input_keys:代表模型输入数据的变量名称,此处填入变量 input_keys
  4. +
  5. label_keys:代表真实标签的变量名称,此处填入变量 output_keys
  6. +
  7. block_size:代表使用多长的时间步进行训练,指定为变量 train_block_size 的值;
  8. +
  9. stride:代表连续的两个训练样本之间的时间步间隔,指定为16;
  10. +
  11. weight_dict:代表模型输出各个变量与真实标签损失函数的权重,此处使用 output_keysweights 生成。
  12. +
+

"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,另外还指定了该类初始化时参数 drop_lastshuffle 均为 True

+

train_dataloader_cfg 还定义了 batch_sizenum_workers 的值。

+

定义监督约束的代码如下:

+
examples/lorenz/train_enn.py
72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELossWithL2Decay(
+        regularization_dict={
+            regularization_key: 1.0e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)
+        }
+    ),
+    {
+        key: lambda out, k=key: out[k]
+        for key in cfg.MODEL.output_keys + (regularization_key,)
+    },
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用带有 L2Decay 的 MSELoss,类名为 MSELossWithL2Decayregularization_dict 设置了正则化的变量名称和对应的权重;

+

第三个参数表示在训练时如何计算需要被约束的中间变量,此处我们约束的变量就是网络的输出;

+

第四个参数是约束条件的名字,方便后续对其索引。此处命名为 "Sup"。

+

3.3.2 模型构建

+

在该案例中,Embedding 模型的输入输出都是物理空间中点的位置坐标 \((x, y, z)\) ,使用了全连接层实现 Embedding 模型,如下图所示。

+
+

lorenz_embedding +

+
Embedding 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/lorenz/train_enn.py
91
+92
+93
+94
+95
+96
+97
data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+model = ppsci.arch.LorenzEmbedding(
+    cfg.MODEL.input_keys,
+    cfg.MODEL.output_keys + (regularization_key,),
+    data_mean,
+    data_std,
+)
+
+

其中,LorenzEmbedding 的前两个参数在前文中已有描述,这里不再赘述,网络模型的第三、四个参数是训练数据集的均值和方差,用于归一化输入数据。计算均值、方差的的代码表示如下:

+
examples/lorenz/train_enn.py
32
+33
+34
+35
+36
+37
+38
+39
def get_mean_std(data: np.ndarray):
+    mean = np.asarray(
+        [np.mean(data[:, :, 0]), np.mean(data[:, :, 1]), np.mean(data[:, :, 2])]
+    ).reshape(1, 3)
+    std = np.asarray(
+        [np.std(data[:, :, 0]), np.std(data[:, :, 1]), np.std(data[:, :, 2])]
+    ).reshape(1, 3)
+    return mean, std
+
+

3.3.3 学习率与优化器构建

+

本案例中使用的学习率方法为 ExponentialDecay ,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下

+
examples/lorenz/train_enn.py
# init optimizer and lr scheduler
+clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    iters_per_epoch=ITERS_PER_EPOCH,
+    decay_steps=ITERS_PER_EPOCH,
+    **cfg.TRAIN.lr_scheduler,
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.3.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/lorenz/train_enn.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "LorenzDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 32,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用 ppsci.metric.MSE

+

3.3.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/lorenz/train_enn.py
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=True,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.4 Transformer 模型

+

上文介绍了如何构建 Embedding 模型的训练、评估,在本节中将介绍如何使用训练好的 Embedding 模型训练 Transformer 模型。因为训练 Transformer 模型的步骤与训练 Embedding 模型的步骤基本相似,因此本节在两者的重复部分的各个参数不再详细介绍。首先将代码中定义的各个参数变量展示如下,每个参数的具体含义会在下面使用到时进行解释。

+
examples/lorenz/conf/transformer.yaml
36
+37
+38
+39
+40
+41
+42
# model settings
+MODEL:
+  input_keys: ["embeds"]
+  output_keys: ["pred_embeds"]
+  num_layers: 4
+  num_ctx: 64
+  embed_size: 32
+
+

3.4.1 约束构建

+

Transformer 模型同样基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/lorenz/train_transformer.py
68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
train_dataloader_cfg = {
+    "dataset": {
+        "name": "LorenzDataset",
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 64,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

数据加载的各个参数与 Embedding 模型中的基本一致,不再赘述。需要说明的是由于 Transformer 模型训练的输入数据是 Embedding 模型 Encoder 模块的输出数据,因此我们将训练好的 Embedding 模型作为 LorenzDataset 的一个参数,在初始化时首先将训练数据映射到编码空间。

+

定义监督约束的代码如下:

+
examples/lorenz/train_transformer.py
87
+88
+89
+90
+91
+92
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.4.2 模型构建

+

在该案例中,Transformer 模型的输入输出都是编码空间中的向量,使用的 Transformer 结构如下:

+
+

lorenz_transformer +

+
Transformer 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/lorenz/train_transformer.py
model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+

PhysformerGPT2 除了需要填入 input_keysoutput_keys 外,还需要设置 Transformer 模型的层数 num_layers、上下文的大小 num_ctx、输入的 Embedding 向量的长度 embed_size、多头注意力机制的参数 num_heads,在这里填入的数值为4、64、32、4。

+

3.4.3 学习率与优化器构建

+

本案例中使用的学习率方法为 CosineWarmRestarts,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下:

+
examples/lorenz/train_transformer.py
clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+    iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.4.4 评估器构建

+

训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。用 PaddleScience 代码表示如下:

+
examples/lorenz/train_transformer.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "LorenzDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

3.4.5 可视化器构建

+

本案例中可以通过构建可视化器在模型评估时将评估结果可视化出来,由于 Transformer 模型的输出数据是预测的编码空间的数据无法直接进行可视化,因此需要额外将输出数据使用 Embedding 网络的 Decoder 模块变换到物理状态空间。

+

在本文中首先定义了对 Transformer 模型输出数据变换到物理状态空间的代码:

+
examples/lorenz/train_transformer.py
34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
def build_embedding_model(embedding_model_path: str) -> ppsci.arch.LorenzEmbedding:
+    input_keys = ("states",)
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.LorenzEmbedding(input_keys, output_keys + (regularization_key,))
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]):
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+
+        return pred_states
+
+
examples/lorenz/train_transformer.py
embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+output_transform = OutputTransform(embedding_model)
+
+

可以看到,程序首先载入了训练好的 Embedding 模型,然后在 OutputTransform__call__ 函数内实现了编码向量到物理状态空间的变换。

+

在定义好了以上代码之后,就可以实现可视化器代码的构建了:

+
examples/lorenz/train_transformer.py
states = mse_validator.data_loader.dataset.data
+embedding_data = mse_validator.data_loader.dataset.embedding_data
+vis_data = {
+    "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+    "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+}
+
+visualizer = {
+    "visualize_states": ppsci.visualize.VisualizerScatter3D(
+        vis_data,
+        {
+            "pred_states": lambda d: output_transform(d),
+            "states": lambda d: d["states"],
+        },
+        num_timestamps=1,
+        prefix="result_states",
+    )
+}
+
+

首先使用上文中的 mse_validator 中的数据集进行可视化,另外还引入了 vis_data_nums 变量用于控制需要可视化样本的数量。最后通过 VisualizerScatter3D 构建可视化器。

+

3.4.6 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/lorenz/train_transformer.py
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
lorenz/train_enn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step1: training a embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def get_mean_std(data: np.ndarray):
+    mean = np.asarray(
+        [np.mean(data[:, :, 0]), np.mean(data[:, :, 1]), np.mean(data[:, :, 2])]
+    ).reshape(1, 3)
+    std = np.asarray(
+        [np.std(data[:, :, 0]), np.std(data[:, :, 1]), np.std(data[:, :, 2])]
+    ).reshape(1, 3)
+    return mean, std
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    weights = (1.0 * (cfg.TRAIN_BLOCK_SIZE - 1), 1.0e4 * cfg.TRAIN_BLOCK_SIZE)
+    regularization_key = "k_matrix"
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 16,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELossWithL2Decay(
+            regularization_dict={
+                regularization_key: 1.0e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)
+            }
+        ),
+        {
+            key: lambda out, k=key: out[k]
+            for key in cfg.MODEL.output_keys + (regularization_key,)
+        },
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+
+    # manually init model
+    data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+    model = ppsci.arch.LorenzEmbedding(
+        cfg.MODEL.input_keys,
+        cfg.MODEL.output_keys + (regularization_key,),
+        data_mean,
+        data_std,
+    )
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+        iters_per_epoch=ITERS_PER_EPOCH,
+        decay_steps=ITERS_PER_EPOCH,
+        **cfg.TRAIN.lr_scheduler,
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    weights = (1.0 * (cfg.VALID_BLOCK_SIZE - 1), 1.0e4 * cfg.VALID_BLOCK_SIZE)
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 32,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=True,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    weights = (1.0 * (cfg.TRAIN_BLOCK_SIZE - 1), 1.0e4 * cfg.TRAIN_BLOCK_SIZE)
+    regularization_key = "k_matrix"
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 16,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELossWithL2Decay(
+            regularization_dict={
+                regularization_key: 1.0e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)
+            }
+        ),
+        {
+            key: lambda out, k=key: out[k]
+            for key in cfg.MODEL.output_keys + (regularization_key,)
+        },
+        name="Sup",
+    )
+
+    # manually init model
+    data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+    model = ppsci.arch.LorenzEmbedding(
+        cfg.MODEL.input_keys,
+        cfg.MODEL.output_keys + (regularization_key,),
+        data_mean,
+        data_std,
+    )
+
+    # manually build validator
+    weights = (1.0 * (cfg.VALID_BLOCK_SIZE - 1), 1.0e4 * cfg.VALID_BLOCK_SIZE)
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 32,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="enn.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
lorenz/train_transformer.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step2: training a transformer model, based on frozen pretrained embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+from typing import Dict
+
+import hydra
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.arch import base
+from ppsci.utils import logger
+from ppsci.utils import save_load
+
+
+def build_embedding_model(embedding_model_path: str) -> ppsci.arch.LorenzEmbedding:
+    input_keys = ("states",)
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.LorenzEmbedding(input_keys, output_keys + (regularization_key,))
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]):
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+
+        return pred_states
+
+
+def train(cfg: DictConfig):
+    # train time-series: 2048    time-steps: 256    block-size: 64  stride: 64
+    # valid time-series: 64      time-steps: 1024   block-size: 256 stride: 1024
+    # test  time-series: 256     time-steps: 1024
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 64,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(constraint["Sup"].data_loader)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+        iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_data = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    visualizer = {
+        "visualize_states": ppsci.visualize.VisualizerScatter3D(
+            vis_data,
+            {
+                "pred_states": lambda d: output_transform(d),
+                "states": lambda d: d["states"],
+            },
+            num_timestamps=1,
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # directly evaluate pretrained model(optional)
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "LorenzDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    visualizer = {
+        "visulzie_states": ppsci.visualize.VisualizerScatter3D(
+            vis_datas,
+            {
+                "pred_states": lambda d: output_transform(d),
+                "states": lambda d: d["states"],
+            },
+            num_timestamps=1,
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction for pretrained model(optional)
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    model_cfg = {
+        **cfg.MODEL,
+        "embedding_model": embedding_model,
+        "input_keys": ["states"],
+        "output_keys": ["pred_states"],
+    }
+    model = ppsci.arch.PhysformerGPT2(**model_cfg)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            key: InputSpec([None, 255, 3], "float32", name=key)
+            for key in model.input_keys
+        },
+    ]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    dataset_cfg = {
+        "name": "LorenzDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+    }
+
+    dataset = ppsci.data.dataset.build_dataset(dataset_cfg)
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, :-1, :],
+    }
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_keys = ["pred_states"]
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    data_dict = {**input_dict, **output_dict}
+    for i in range(cfg.VIS_DATA_NUMS):
+        ppsci.visualize.save_plot_from_3d_dict(
+            f"./lorenz_transformer_pred_{i}",
+            {key: value[i] for key, value in data_dict.items()},
+            ("states", "pred_states"),
+        )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="transformer.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下图中展示了两个不同初始条件下的模型预测结果和传统数值微分的预测结果。

+
+

result_states0 +

+
模型预测结果("pred_states")与传统数值微分结果("states")
+
+
+

result_states1 +

+
模型预测结果("pred_states")与传统数值微分结果("states")
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/nlsmb/index.html b/zh/examples/nlsmb/index.html new file mode 100644 index 0000000000..560e5f1e5d --- /dev/null +++ b/zh/examples/nlsmb/index.html @@ -0,0 +1,5059 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NLSMB - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

NLS-MB

+ + +
+
+
+
# soliton
+python NLS-MB_optical_soliton.py
+# rogue wave
+python NLS-MB_optical_rogue_wave.py
+
+
+
+
# soliton
+python NLS-MB_optical_soliton.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/NLS-MB/NLS-MB_soliton_pretrained.pdparams
+# rogue wave
+python NLS-MB_optical_rogue_wave.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/NLS-MB/NLS-MB_rogue_wave_pretrained.pdparams
+
+
+
+
# soliton
+python NLS-MB_optical_soliton.py mode=export
+# rogue wave
+python NLS-MB_optical_rogue_wave.py mode=export
+
+
+
+
# soliton
+python NLS-MB_optical_soliton.py mode=infer
+# rogue wave
+python NLS-MB_optical_rogue_wave.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + + + + + +
预训练模型指标
NLS-MB_soliton_pretrained.pdparamsResidual/loss: 0.00000
Residual/MSE.Schrodinger_1: 0.00000
Residual/MSE.Schrodinger_2: 0.00000
Residual/MSE.Maxwell_1: 0.00000
Residual/MSE.Maxwell_2: 0.00000
Residual/MSE.Bloch: 0.00000
NLS-MB_optical_rogue_wave.pdparamsResidual/loss: 0.00001
Residual/MSE.Schrodinger_1: 0.00000
Residual/MSE.Schrodinger_2: 0.00000
Residual/MSE.Maxwell_1: 0.00000
Residual/MSE.Maxwell_2: 0.00000
Residual/MSE.Bloch: 0.00000
+

1. 背景简介

+

非线性局域波动力学,作为非线性科学的重要分支,涵盖了孤子、呼吸子和怪波等基本形式的非线性局域波。激光锁模技术为这些理论预言的非线性局域波提供了实验验证的平台,人们通过此技术观察到了孤子分子和怪波等丰富的非线性现象,进一步推动了非线性局域波的研究。目前,该领域的研究已深入流体力学、非线性光学、玻色-爱因斯坦凝聚(BEC)、等离子体物理等多个物理领域。在光纤领域,非线性动力学的研究基于光纤的光学器件、信息处理、材料设计以及信号传输的原理,对光纤激光器、放大器、波导和通信技术的发展起到了关键作用。光脉冲在光纤中的传播动力学受非线性偏微分方程(如非线性薛定谔方程NLSE)的调控。当色散与非线性效应共存时,这些方程往往难以解析求解。因此,分步傅立叶方法及其改进版本被广泛应用于研究光纤中的非线性效应,其优势在于实现简单且具有较高的相对精度。然而,对于长距离且高度非线性的场景,为满足精度需求,必须大幅减少分步傅立叶方法的步长,这无疑增加了计算复杂性,导致时域中网格点集数量庞大,计算过程耗时较长。PINN比数据驱动的方法在数据少得多的情况下表现出更好的性能,并且计算复杂性(以倍数表示)通常比SFM低两个数量级。

+

2. 问题定义

+

在掺铒光纤中,光脉冲的传播性质可以由耦合NLS-MB方程来描述,其形式为

+
\[ +\begin{cases} + \dfrac{\partial E}{\partial x} = i \alpha_1 \dfrac{\partial^2 E}{\partial t ^2} - i \alpha_2 |E|^2 E+2 p \\ + \dfrac{\partial p}{\partial t} = 2 i \omega_0 p+2 E \eta \\ + \dfrac{\partial \eta}{\partial t} = -(E p^* + E^* p) +\end{cases} +\]
+

其中,x, t分别表示归一化的传播距离和时间,复包络E是慢变的电场,p是共振介质偏振的量度,\(\eta\)表示粒子数反转的程度,符号*表示复共轭。\(\alpha_1\)是群速度色散参数,\(\alpha_2\)​​是Kerr非线性参数,是测量共振频率的偏移。NLS-MB系统是由Maimistov和Manykin首次提出来的,用来描述极短的脉冲在Kerr非线性介质中的传播.该系统在解决光纤损耗使得其传输距离受限这一问题上,也扮演着重要的作用。在这个方程中,它描述的是自感应透明孤子和NLS孤子的混合状态,称作SIT-NLS孤子,这两种孤子可以共存,并且已经有很多关于其在光纤通信中的研究.

+

2.1 Optical soliton

+

在光纤的反常色散区,由于色散和非线性效应的相互作用,可产生一种非常引人注目的现象——光孤子。“孤子”(soliton)是一种特殊的波包,它可以传输很长距离而不变形孤子在物理学的许多分支已得到广的研究,本案例讨论的光纤中的孤子不仅具有基础理论研究价值,而且在光纤通信方面也有实际应用。

+
\[ +\begin{gathered} + E(x,t) = \frac{{2\exp ( - 2it)}}{{\cosh (2t + 6x)}}, \\ + p(x,t) = \frac{{\exp ( - 2it)\left\{ {\exp ( - 2t - 6x) - \exp (2t + 6x)} \right\}}}{{\cosh {{(2t + 6x)}^2}}}, \\ + \eta (x,t) = \frac{{\cosh {{(2t + 6x)}^2} - 2}}{{\cosh {{(2t + 6x)}^2}}}. +\end{gathered} +\]
+

我们考虑计算域为 \([−1, 1] × [−1, 1]\)。 我们首先确定优化策略。 每个边界上有 \(200\) 个点,即 \(N_b = 2 × 200\)。为了计算 NLS-MB 的方程损失,在域内随机选择 \(20,000\) 个点。

+

2.2 Optical rogue wave

+

光学怪波(Optical rogue waves)是光学中的一种现象,类似于海洋中的孤立海浪,但在光学系统中。它们是突然出现并且幅度异常高的光波,光学孤立子波有一些潜在的应用,尤其是在光通信和激光技术领域。一些研究表明,它们可以用于增强光信号的传输和处理,或者用于产生超短脉冲激光。 +我们考虑计算域为 \([−0.5, 0.5] × [−2.5, 2.5]\)

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

本文使用PINN经典的MLP模型进行训练。

+
model = ppsci.arch.MLP(**cfg.MODEL)
+
+

3.2 方程构建

+

由于 Optical soliton 使用的是 NLS-MB 方程,因此可以直接使用 PaddleScience 内置的 NLSMB

+
97
+98
+99
equation = {
+    "NLS-MB": ppsci.equation.NLSMB(alpha_1=0.5, alpha_2=-1, omega_0=-1, time=True)
+}
+
+

3.3 计算域构建

+

本文中 Optical soliton 问题作用在以空间(-1.0, 1.0), 时间(-1.0, 1.0) 的时空区域, +因此可以直接使用 PaddleScience 内置的时空几何 time_interval 作为计算域。

+
geom = {
+    "time_interval": ppsci.geometry.TimeXGeometry(
+        ppsci.geometry.TimeDomain(t_lower, t_upper, timestamps=timestamps),
+        ppsci.geometry.Interval(x_lower, x_upper),
+    )
+}
+
+

3.4 约束构建

+

因数据集为解析解,我们先构造解析解函数

+
26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
def analytic_solution(out):
+    t, x = out["t"], out["x"]
+    Eu_true = 2 * np.cos(2 * t) / np.cosh(2 * t + 6 * x)
+
+    Ev_true = -2 * np.sin(2 * t) / np.cosh(2 * t + 6 * x)
+
+    pu_true = (
+        (np.exp(-2 * t - 6 * x) - np.exp(2 * t + 6 * x))
+        * np.cos(2 * t)
+        / np.cosh(2 * t + 6 * x) ** 2
+    )
+    pv_true = (
+        -(np.exp(-2 * t - 6 * x) - np.exp(2 * t + 6 * x))
+        * np.sin(2 * t)
+        / np.cosh(2 * t + 6 * x) ** 2
+    )
+    eta_true = (np.cosh(2 * t + 6 * x) ** 2 - 2) / np.cosh(2 * t + 6 * x) ** 2
+
+    return Eu_true, Ev_true, pu_true, pv_true, eta_true
+
+

3.4.1 内部点约束

+

以作用在内部点上的 InteriorConstraint 为例,代码如下:

+
pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["NLS-MB"].equations,
+    {
+        "Schrodinger_1": 0,
+        "Schrodinger_2": 0,
+        "Maxwell_1": 0,
+        "Maxwell_2": 0,
+        "Bloch": 0,
+    },
+    geom["time_interval"],
+    {
+        "dataset": {"name": "IterableNamedArrayDataset"},
+        "batch_size": 20000,
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss(),
+    evenly=True,
+    name="EQ",
+)
+
+

InteriorConstraint 的第一个参数是方程(组)表达式,用于描述如何计算约束目标,此处填入在 3.2 方程构建 章节中实例化好的 equation["NLS-MB"].equations

+

第二个参数是约束变量的目标值,在本问题中希望 NLS-MB 每个方程均被优化至 0;

+

第三个参数是约束方程作用的计算域,此处填入在 3.3 计算域构建 章节实例化好的 geom["time_interval"] 即可;

+

第四个参数是在计算域上的采样配置,此处设置 batch_size20000

+

第五个参数是损失函数,此处选用常用的 MSE 函数,且 reduction 设置为 "mean",即会将参与计算的所有数据点的均方误差;

+

第六个参数是约束条件的名字,需要给每一个约束条件命名,方便后续对其索引。此处命名为 "EQ" 即可。

+

3.4.2 边界约束

+

由于我们边界点和初值点具有解析解,因此我们使用监督约束

+
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELoss("mean"),
+    name="Sup",
+)
+
+

3.5 超参数设定

+

接下来需要指定训练轮数和学习率,此处按实验经验,使用 50000 轮训练轮数,0.001 的初始学习率。

+
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
# training settings
+TRAIN:
+  epochs: 50000
+  iters_per_epoch: 1
+  lbfgs:
+    iters_per_epoch: ${TRAIN.iters_per_epoch}
+    output_dir: ${output_dir}LBFGS
+    learning_rate: 1.0
+    max_iter: 1
+    eval_freq: ${TRAIN.eval_freq}
+    eval_during_train: ${TRAIN.eval_during_train}
+  eval_during_train: true
+  eval_freq: 1000
+  learning_rate: 0.001
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
residual_validator = ppsci.validate.GeometryValidator(
+    equation["NLS-MB"].equations,
+    {
+        "Schrodinger_1": 0,
+        "Schrodinger_2": 0,
+        "Maxwell_1": 0,
+        "Maxwell_2": 0,
+        "Bloch": 0,
+    },
+    geom["time_interval"],
+    {
+        "dataset": "IterableNamedArrayDataset",
+        "total_size": 20600,
+    },
+    ppsci.loss.MSELoss(),
+    evenly=True,
+    metric={"MSE": ppsci.metric.MSE()},
+    with_initial=True,
+    name="Residual",
+)
+validator = {residual_validator.name: residual_validator}
+
+

3.8 可视化器构建

+

在模型训练完毕之后,我们可以在计算域取点进行预测,并手动计算出振幅,并可视化结果。

+
vis_points = geom["time_interval"].sample_interior(20000, evenly=True)
+Eu_true, Ev_true, pu_true, pv_true, eta_true = analytic_solution(vis_points)
+pred = solver.predict(vis_points, return_numpy=True)
+t = vis_points["t"][:, 0]
+x = vis_points["x"][:, 0]
+E_ref = np.sqrt(Eu_true**2 + Ev_true**2)[:, 0]
+E_pred = np.sqrt(pred["Eu"] ** 2 + pred["Ev"] ** 2)[:, 0]
+p_ref = np.sqrt(pu_true**2 + pv_true**2)[:, 0]
+p_pred = np.sqrt(pred["pu"] ** 2 + pred["pv"] ** 2)[:, 0]
+eta_ref = eta_true[:, 0]
+eta_pred = pred["eta"][:, 0]
+
+# plot
+plot(t, x, E_ref, E_pred, p_ref, p_pred, eta_ref, eta_pred, cfg.output_dir)
+
+

3.9 模型训练、评估与可视化

+

3.9.1 使用 Adam 训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.9.2 使用 L-BFGS 微调[可选]

+

在使用 Adam 优化器训练完毕之后,我们可以将优化器更换成二阶优化器 L-BFGS 继续训练少量轮数(此处我们使用 Adam 优化轮数的 10% 即可),从而进一步提高模型精度。

+
OUTPUT_DIR = cfg.TRAIN.lbfgs.output_dir
+logger.init_logger("ppsci", osp.join(OUTPUT_DIR, f"{cfg.mode}.log"), "info")
+EPOCHS = cfg.TRAIN.epochs // 10
+optimizer_lbfgs = ppsci.optimizer.LBFGS(
+    cfg.TRAIN.lbfgs.learning_rate, cfg.TRAIN.lbfgs.max_iter
+)(model)
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer_lbfgs,
+    None,
+    EPOCHS,
+    cfg.TRAIN.lbfgs.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.lbfgs.eval_during_train,
+    eval_freq=cfg.TRAIN.lbfgs.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+
+提示 +

在常规优化器训练完毕之后,使用 L-BFGS 微调少量轮数的方法,在大多数场景中都可以进一步有效提高模型精度。

+
+

4. 完整代码

+
NLS-MB_optical_soliton.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import numpy as np
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def analytic_solution(out):
+    t, x = out["t"], out["x"]
+    Eu_true = 2 * np.cos(2 * t) / np.cosh(2 * t + 6 * x)
+
+    Ev_true = -2 * np.sin(2 * t) / np.cosh(2 * t + 6 * x)
+
+    pu_true = (
+        (np.exp(-2 * t - 6 * x) - np.exp(2 * t + 6 * x))
+        * np.cos(2 * t)
+        / np.cosh(2 * t + 6 * x) ** 2
+    )
+    pv_true = (
+        -(np.exp(-2 * t - 6 * x) - np.exp(2 * t + 6 * x))
+        * np.sin(2 * t)
+        / np.cosh(2 * t + 6 * x) ** 2
+    )
+    eta_true = (np.cosh(2 * t + 6 * x) ** 2 - 2) / np.cosh(2 * t + 6 * x) ** 2
+
+    return Eu_true, Ev_true, pu_true, pv_true, eta_true
+
+
+def plot(
+    t: np.ndarray,
+    x: np.ndarray,
+    E_ref: np.ndarray,
+    E_pred: np.ndarray,
+    p_ref: np.ndarray,
+    p_pred: np.ndarray,
+    eta_ref: np.ndarray,
+    eta_pred: np.ndarray,
+    output_dir: str,
+):
+    fig = plt.figure(figsize=(10, 10))
+    plt.subplot(3, 3, 1)
+    plt.title("E_ref")
+    plt.tricontourf(x, t, E_ref, levels=256, cmap="jet")
+    plt.subplot(3, 3, 2)
+    plt.title("E_pred")
+    plt.tricontourf(x, t, E_pred, levels=256, cmap="jet")
+    plt.subplot(3, 3, 3)
+    plt.title("E_diff")
+    plt.tricontourf(x, t, np.abs(E_ref - E_pred), levels=256, cmap="jet")
+    plt.subplot(3, 3, 4)
+    plt.title("p_ref")
+    plt.tricontourf(x, t, p_ref, levels=256, cmap="jet")
+    plt.subplot(3, 3, 5)
+    plt.title("p_pred")
+    plt.tricontourf(x, t, p_pred, levels=256, cmap="jet")
+    plt.subplot(3, 3, 6)
+    plt.title("p_diff")
+    plt.tricontourf(x, t, np.abs(p_ref - p_pred), levels=256, cmap="jet")
+    plt.subplot(3, 3, 7)
+    plt.title("eta_ref")
+    plt.tricontourf(x, t, eta_ref, levels=256, cmap="jet")
+    plt.subplot(3, 3, 8)
+    plt.title("eta_pred")
+    plt.tricontourf(x, t, eta_pred, levels=256, cmap="jet")
+    plt.subplot(3, 3, 9)
+    plt.title("eta_diff")
+    plt.tricontourf(x, t, np.abs(eta_ref - eta_pred), levels=256, cmap="jet")
+    fig_path = osp.join(output_dir, "pred_optical_soliton.png")
+    print(f"Saving figure to {fig_path}")
+    fig.savefig(fig_path, bbox_inches="tight", dpi=400)
+    plt.close()
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {
+        "NLS-MB": ppsci.equation.NLSMB(alpha_1=0.5, alpha_2=-1, omega_0=-1, time=True)
+    }
+
+    x_lower = -1
+    x_upper = 1
+    t_lower = -1
+    t_upper = 1
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(t_lower, t_upper, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_interval": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(t_lower, t_upper, timestamps=timestamps),
+            ppsci.geometry.Interval(x_lower, x_upper),
+        )
+    }
+
+    X, T = np.meshgrid(
+        np.linspace(x_lower, x_upper, 256), np.linspace(t_lower, t_upper, 256)
+    )
+    X_star = np.hstack((X.flatten()[:, None], T.flatten()[:, None]))
+
+    # Boundary and Initial conditions
+    ic = X_star[:, 1] == t_lower
+    idx_ic = np.random.choice(np.where(ic)[0], 200, replace=False)
+    lb = X_star[:, 0] == x_lower
+    idx_lb = np.random.choice(np.where(lb)[0], 200, replace=False)
+    ub = X_star[:, 0] == x_upper
+    idx_ub = np.random.choice(np.where(ub)[0], 200, replace=False)
+    icbc_idx = np.hstack((idx_lb, idx_ic, idx_ub))
+    X_u_train = X_star[icbc_idx].astype("float32")
+    X_u_train = {"t": X_u_train[:, 1:2], "x": X_u_train[:, 0:1]}
+
+    Eu_train, Ev_train, pu_train, pv_train, eta_train = analytic_solution(X_u_train)
+
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"t": X_u_train["t"], "x": X_u_train["x"]},
+            "label": {
+                "Eu": Eu_train,
+                "Ev": Ev_train,
+                "pu": pu_train,
+                "pv": pv_train,
+                "eta": eta_train,
+            },
+        },
+        "batch_size": 600,
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    }
+
+    # set constraint
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NLS-MB"].equations,
+        {
+            "Schrodinger_1": 0,
+            "Schrodinger_2": 0,
+            "Maxwell_1": 0,
+            "Maxwell_2": 0,
+            "Bloch": 0,
+        },
+        geom["time_interval"],
+        {
+            "dataset": {"name": "IterableNamedArrayDataset"},
+            "batch_size": 20000,
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        name="EQ",
+    )
+
+    # supervised constraint s.t ||u-u_0||
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss("mean"),
+        name="Sup",
+    )
+
+    # wrap constraints together
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        sup_constraint.name: sup_constraint,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(learning_rate=cfg.TRAIN.learning_rate)(model)
+
+    # set validator
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NLS-MB"].equations,
+        {
+            "Schrodinger_1": 0,
+            "Schrodinger_2": 0,
+            "Maxwell_1": 0,
+            "Maxwell_2": 0,
+            "Bloch": 0,
+        },
+        geom["time_interval"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": 20600,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    # fine-tuning pretrained model with L-BFGS
+    OUTPUT_DIR = cfg.TRAIN.lbfgs.output_dir
+    logger.init_logger("ppsci", osp.join(OUTPUT_DIR, f"{cfg.mode}.log"), "info")
+    EPOCHS = cfg.TRAIN.epochs // 10
+    optimizer_lbfgs = ppsci.optimizer.LBFGS(
+        cfg.TRAIN.lbfgs.learning_rate, cfg.TRAIN.lbfgs.max_iter
+    )(model)
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        OUTPUT_DIR,
+        optimizer_lbfgs,
+        None,
+        EPOCHS,
+        cfg.TRAIN.lbfgs.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.lbfgs.eval_during_train,
+        eval_freq=cfg.TRAIN.lbfgs.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+    # visualize prediction
+    vis_points = geom["time_interval"].sample_interior(20000, evenly=True)
+    Eu_true, Ev_true, pu_true, pv_true, eta_true = analytic_solution(vis_points)
+    pred = solver.predict(vis_points, return_numpy=True)
+    t = vis_points["t"][:, 0]
+    x = vis_points["x"][:, 0]
+    E_ref = np.sqrt(Eu_true**2 + Ev_true**2)[:, 0]
+    E_pred = np.sqrt(pred["Eu"] ** 2 + pred["Ev"] ** 2)[:, 0]
+    p_ref = np.sqrt(pu_true**2 + pv_true**2)[:, 0]
+    p_pred = np.sqrt(pred["pu"] ** 2 + pred["pv"] ** 2)[:, 0]
+    eta_ref = eta_true[:, 0]
+    eta_pred = pred["eta"][:, 0]
+
+    # plot
+    plot(t, x, E_ref, E_pred, p_ref, p_pred, eta_ref, eta_pred, cfg.output_dir)
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {
+        "NLS-MB": ppsci.equation.NLSMB(alpha_1=0.5, alpha_2=-1, omega_0=-1, time=True)
+    }
+
+    # set geometry
+    x_lower = -1
+    x_upper = 1
+    t_lower = -1
+    t_upper = 1
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(t_lower, t_upper, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_interval": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(t_lower, t_upper, timestamps=timestamps),
+            ppsci.geometry.Interval(x_lower, x_upper),
+        )
+    }
+
+    # set validator
+    residual_validator = ppsci.validate.GeometryValidator(
+        equation["NLS-MB"].equations,
+        {
+            "Schrodinger_1": 0,
+            "Schrodinger_2": 0,
+            "Maxwell_1": 0,
+            "Maxwell_2": 0,
+            "Bloch": 0,
+        },
+        geom["time_interval"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": 20600,
+        },
+        ppsci.loss.MSELoss(),
+        evenly=True,
+        metric={"MSE": ppsci.metric.MSE()},
+        with_initial=True,
+        name="Residual",
+    )
+    validator = {residual_validator.name: residual_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+
+    # visualize prediction
+    vis_points = geom["time_interval"].sample_interior(20000, evenly=True)
+    Eu_true, Ev_true, pu_true, pv_true, eta_true = analytic_solution(vis_points)
+    pred = solver.predict(vis_points, return_numpy=True)
+    t = vis_points["t"][:, 0]
+    x = vis_points["x"][:, 0]
+    E_ref = np.sqrt(Eu_true**2 + Ev_true**2)[:, 0]
+    E_pred = np.sqrt(pred["Eu"] ** 2 + pred["Ev"] ** 2)[:, 0]
+    p_ref = np.sqrt(pu_true**2 + pv_true**2)[:, 0]
+    p_pred = np.sqrt(pred["pu"] ** 2 + pred["pv"] ** 2)[:, 0]
+    eta_ref = eta_true[:, 0]
+    eta_pred = pred["eta"][:, 0]
+
+    # plot
+    plot(t, x, E_ref, E_pred, p_ref, p_pred, eta_ref, eta_pred, cfg.output_dir)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    x_lower = -1
+    x_upper = 1
+    t_lower = -1
+    t_upper = 1
+    # set timestamps(including initial t0)
+    timestamps = np.linspace(t_lower, t_upper, cfg.NTIME_ALL, endpoint=True)
+    # set time-geometry
+    geom = {
+        "time_interval": ppsci.geometry.TimeXGeometry(
+            ppsci.geometry.TimeDomain(t_lower, t_upper, timestamps=timestamps),
+            ppsci.geometry.Interval(x_lower, x_upper),
+        )
+    }
+
+    NPOINT_TOTAL = cfg.NPOINT_INTERIOR + cfg.NPOINT_BC
+    input_dict = geom["time_interval"].sample_interior(NPOINT_TOTAL, evenly=True)
+
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    # visualize prediction
+    Eu_true, Ev_true, pu_true, pv_true, eta_true = analytic_solution(input_dict)
+    t = input_dict["t"][:, 0]
+    x = input_dict["x"][:, 0]
+    E_ref = np.sqrt(Eu_true**2 + Ev_true**2)[:, 0]
+    E_pred = np.sqrt(output_dict["Eu"] ** 2 + output_dict["Ev"] ** 2)[:, 0]
+    p_ref = np.sqrt(pu_true**2 + pv_true**2)[:, 0]
+    p_pred = np.sqrt(output_dict["pu"] ** 2 + output_dict["pv"] ** 2)[:, 0]
+    eta_ref = eta_true[:, 0]
+    eta_pred = output_dict["eta"][:, 0]
+
+    # plot
+    plot(t, x, E_ref, E_pred, p_ref, p_pred, eta_ref, eta_pred, cfg.output_dir)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="NLS-MB_soliton.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

5.1 optical_soliton

+
+

optical_soliton +

+
解析解结果与 PINN 预测结果对比,从上到下分别为:慢变电场(E),共振偏量(p)以及粒子数反转程度(eta)
+
+

5.2 optical_rogue_wave

+
+

optical_rogue_wave +

+
解析解结果与 PINN 预测结果对比,从上到下分别为:慢变电场(E),共振偏量(p)以及粒子数反转程度(eta)
+
+

可以看到PINN预测与解析解的结果基本一致。

+

6. 参考资料

+

[1] S.-Y. Xu, Q. Zhou, and W. Liu, Prediction of Soliton Evolution and Equation Parameters for NLS–MB Equation Based on the phPINN Algorithm, Nonlinear Dyn (2023).

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/nowcastnet/index.html b/zh/examples/nowcastnet/index.html new file mode 100644 index 0000000000..002b2ebebf --- /dev/null +++ b/zh/examples/nowcastnet/index.html @@ -0,0 +1,3980 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NowcastNet - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

NowcastNet

+
+
+
+

暂无

+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/nowcastnet/mrms.tar
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/nowcastnet/mrms.tar -o mrms.tar
+mkdir ./datasets
+tar -xvf mrms.tar -C ./datasets/
+python nowcastnet.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/nowcastnet/nowcastnet_pretrained.pdparams
+
+
+
+
python nowcastnet.py mode=export
+
+
+
+
python nowcastnet.py mode=infer
+
+
+
+
+

1. 背景简介

+

近年来,深度学习方法已被应用于天气预报,尤其是雷达观测的降水预报。这些方法利用大量雷达复合观测数据来训练神经网络模型,以端到端的方式进行训练,无需明确参考降水过程的物理定律。 +这里复现了一个针对极端降水的非线性短临预报模型——NowcastNet,该模型将物理演变方案和条件学习法统一到一个神经网络框架中,实现了端到端的优化。

+

2. 模型原理

+

本章节仅对 NowcastNet 的模型原理进行简单地介绍,详细的理论推导请阅读 Skilful nowcasting of extreme precipitation with NowcastNet

+

模型的总体结构如图所示:

+
+

nowcastnet-arch +

+
NowcastNet 网络模型
+
+

模型使用预训练权重推理,接下来将介绍模型的推理过程。

+

3. 模型构建

+

在该案例中,用 PaddleScience 代码表示如下:

+
examples/nowcastnet/nowcastnet.py
24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
if cfg.CASE_TYPE == "large":
+    dataset_path = cfg.LARGE_DATASET_PATH
+    model_cfg = cfg.MODEL.large
+    output_dir = osp.join(cfg.output_dir, "large")
+elif cfg.CASE_TYPE == "normal":
+    dataset_path = cfg.NORMAL_DATASET_PATH
+    model_cfg = cfg.MODEL.normal
+    output_dir = osp.join(cfg.output_dir, "normal")
+else:
+    raise ValueError(
+        f"cfg.CASE_TYPE should in ['normal', 'large'], but got '{cfg.mode}'"
+    )
+model = ppsci.arch.NowcastNet(**model_cfg)
+
+
examples/nowcastnet/conf/nowcastnet.yaml
35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
CPU_WORKER: 1
+
+# model settings
+MODEL:
+  normal:
+    input_keys: ["input"]
+    output_keys: ["output"]
+    input_length: 9
+    total_length: 29
+    image_width: 512
+    image_height: 512
+    image_ch: 2
+    ngf: 32
+  large:
+    input_keys: ["input"]
+    output_keys: ["output"]
+    input_length: 9
+    total_length: 29
+    image_width: 1024
+
+

其中,input_keysoutput_keys 分别代表网络模型输入、输出变量的名称。

+

4. 模型评估可视化

+

完成上述设置之后,将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
examples/nowcastnet/nowcastnet.py
57
+58
+59
+60
+61
solver = ppsci.solver.Solver(
+    model,
+    output_dir=output_dir,
+    pretrained_model_path=cfg.EVAL.pretrained_model_path,
+)
+
+

然后构建 VisualizerRadar 生成图片结果:

+
examples/nowcastnet/nowcastnet.py
69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
visualizer = {
+    "v_nowcastnet": ppsci.visualize.VisualizerRadar(
+        {"input": frames_tensor},
+        {
+            "output": lambda out: out["output"],
+        },
+        prefix="v_nowcastnet",
+        case_type=cfg.CASE_TYPE,
+        total_length=model_cfg.total_length,
+    )
+}
+solver.visualizer = visualizer
+# visualize prediction
+solver.visualize(batch_id)
+
+

5. 完整代码

+
examples/nowcastnet/nowcastnet.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
"""
+Reference: https://codeocean.com/capsule/3935105/tree/v1
+"""
+from os import path as osp
+
+import hydra
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    print("Not supported.")
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    if cfg.CASE_TYPE == "large":
+        dataset_path = cfg.LARGE_DATASET_PATH
+        model_cfg = cfg.MODEL.large
+        output_dir = osp.join(cfg.output_dir, "large")
+    elif cfg.CASE_TYPE == "normal":
+        dataset_path = cfg.NORMAL_DATASET_PATH
+        model_cfg = cfg.MODEL.normal
+        output_dir = osp.join(cfg.output_dir, "normal")
+    else:
+        raise ValueError(
+            f"cfg.CASE_TYPE should in ['normal', 'large'], but got '{cfg.mode}'"
+        )
+    model = ppsci.arch.NowcastNet(**model_cfg)
+
+    input_keys = ("radar_frames",)
+    dataset_param = {
+        "input_keys": input_keys,
+        "label_keys": (),
+        "image_width": model_cfg.image_width,
+        "image_height": model_cfg.image_height,
+        "total_length": model_cfg.total_length,
+        "dataset_path": dataset_path,
+        "data_type": paddle.get_default_dtype(),
+    }
+    test_data_loader = paddle.io.DataLoader(
+        ppsci.data.dataset.RadarDataset(**dataset_param),
+        batch_size=1,
+        shuffle=False,
+        num_workers=cfg.CPU_WORKER,
+        drop_last=True,
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=output_dir,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+
+    for batch_id, test_ims in enumerate(test_data_loader):
+        test_ims = test_ims[0][input_keys[0]].numpy()
+        frames_tensor = paddle.to_tensor(
+            data=test_ims, dtype=paddle.get_default_dtype()
+        )
+        if batch_id <= cfg.NUM_SAVE_SAMPLES:
+            visualizer = {
+                "v_nowcastnet": ppsci.visualize.VisualizerRadar(
+                    {"input": frames_tensor},
+                    {
+                        "output": lambda out: out["output"],
+                    },
+                    prefix="v_nowcastnet",
+                    case_type=cfg.CASE_TYPE,
+                    total_length=model_cfg.total_length,
+                )
+            }
+            solver.visualizer = visualizer
+            # visualize prediction
+            solver.visualize(batch_id)
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set models
+    if cfg.CASE_TYPE == "large":
+        model_cfg = cfg.MODEL.large
+    elif cfg.CASE_TYPE == "normal":
+        model_cfg = cfg.MODEL.normal
+    else:
+        raise ValueError(
+            f"cfg.CASE_TYPE should in ['normal', 'large'], but got '{cfg.mode}'"
+        )
+    model = ppsci.arch.NowcastNet(**model_cfg)
+
+    # load pretrained model
+    solver = ppsci.solver.Solver(
+        model=model, pretrained_model_path=cfg.INFER.pretrained_model_path
+    )
+    # export models
+    input_spec = [
+        {
+            key: InputSpec(
+                [None, 29, model_cfg.image_width, model_cfg.image_height, 2],
+                "float32",
+                name=key,
+            )
+            for key in model_cfg.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    import os.path as osp
+
+    from deploy.python_infer import pinn_predictor
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    if cfg.CASE_TYPE == "large":
+        dataset_path = cfg.LARGE_DATASET_PATH
+        model_cfg = cfg.MODEL.large
+        output_dir = osp.join(cfg.output_dir, "large")
+    elif cfg.CASE_TYPE == "normal":
+        dataset_path = cfg.NORMAL_DATASET_PATH
+        model_cfg = cfg.MODEL.normal
+        output_dir = osp.join(cfg.output_dir, "normal")
+    else:
+        raise ValueError(
+            f"cfg.CASE_TYPE should in ['normal', 'large'], but got '{cfg.mode}'"
+        )
+
+    input_keys = ("radar_frames",)
+    dataset_param = {
+        "input_keys": input_keys,
+        "label_keys": (),
+        "image_width": model_cfg.image_width,
+        "image_height": model_cfg.image_height,
+        "total_length": model_cfg.total_length,
+        "dataset_path": dataset_path,
+    }
+    test_data_loader = paddle.io.DataLoader(
+        ppsci.data.dataset.RadarDataset(**dataset_param),
+        batch_size=cfg.INFER.batch_size,
+        num_workers=cfg.CPU_WORKER,
+        drop_last=True,
+    )
+    for batch_id, test_ims in enumerate(test_data_loader):
+        if batch_id > cfg.NUM_SAVE_SAMPLES:
+            break
+        test_ims = {"input": test_ims[0][input_keys[0]].numpy()}
+        output_dict = predictor.predict(test_ims, cfg.INFER.batch_size)
+        # mapping data to model_cfg.output_keys
+        output_dict = {
+            store_key: output_dict[infer_key]
+            for store_key, infer_key in zip(model_cfg.output_keys, output_dict.keys())
+        }
+
+        visualizer = ppsci.visualize.VisualizerRadar(
+            test_ims,
+            {
+                "output": lambda out: out["output"],
+            },
+            prefix="v_nowcastnet",
+            case_type=cfg.CASE_TYPE,
+            total_length=model_cfg.total_length,
+        )
+        test_ims.update(output_dict)
+        visualizer.save(osp.join(output_dir, f"epoch_{batch_id}"), test_ims)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="nowcastnet.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

6. 结果展示

+

下图展示了模型的预测结果和真值结果。

+
+

result +

+
模型预测结果
+
+
+

result +

+
模型真值结果
+
+

7. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/nsfnet/index.html b/zh/examples/nsfnet/index.html new file mode 100644 index 0000000000..f7f46e77d8 --- /dev/null +++ b/zh/examples/nsfnet/index.html @@ -0,0 +1,7012 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSFNets - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

NSFNets

+

AI Studio快速体验

+
+
+
+
# VP_NSFNet1
+python VP_NSFNet1.py    mode=eval  pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/nsfnet/nsfnet1.pdparams
+
+# VP_NSFNet2
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/cylinder_nektar_wake.mat -P ./data/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/cylinder_nektar_wake.mat --create-dirs -o ./data/cylinder_nektar_wake.mat
+
+python VP_NSFNet2.py    mode=eval  data_dir=./data/cylinder_nektar_wake.mat  pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/nsfnet/nsfnet2.pdparams
+
+# VP_NSFNet3
+python VP_NSFNet3.py    mode=eval  pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/nsfnet/nsfnet3.pdparams
+
+
+
+
# VP_NSFNet1
+python VP_NSFNet1.py
+
+# VP_NSFNet2
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/cylinder_nektar_wake.mat -P ./data/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/cylinder_nektar_wake.mat --create-dirs -o ./data/cylinder_nektar_wake.mat
+python VP_NSFNet2.py data_dir=./data/cylinder_nektar_wake.mat
+
+# VP_NSFNet3
+python VP_NSFNet3.py
+
+
+
+
+

1. 背景简介

+

最近几年,深度学习在很多领域取得了非凡的成就,尤其是计算机视觉和自然语言处理方面,而受启发于深度学习的快速发展,基于深度学习强大的函数逼近能力,神经网络在科学计算领域也取得了成功,现阶段的研究主要分为两大类,一类是将物理信息以及物理限制加入损失函数来对神经网络进行训练, 其代表有 PINN 以及 Deep Retz Net,另一类是通过数据驱动的深度神经网络算子,其代表有 FNO 以及 DeepONet。这些方法都在科学实践中获得了广泛应用,比如天气预测,量子化学,生物工程,以及计算流体等领域。而为充分探索PINN对流体方程的求解能力,本次复现论文作者设计了NSFNets,并且先后使用具有解析解或数值解的二维、三维纳韦斯托克方程以及使用DNS方法进行高精度求解的数据集作为参考, 进行正问题求解训练。论文实验表明PINN对不可压纳韦斯托克方程具有优秀的数值求解能力, 本项目主要目标是使用PaddleScience复现论文所实现的高精度求解纳韦斯托克方程的代码。

+

2. 问题定义

+

本问题所使用的为最经典的PINN模型,对此不再赘述。

+

主要介绍所求解的几类纳韦斯托克方程:

+

不可压纳韦斯托克方程可以表示为:

+
\[\frac{\partial \mathbf{u}}{\partial t}+(\mathbf{u} \cdot \nabla) \mathbf{u} =-\nabla p+\frac{1}{Re} \nabla^2 \mathbf{u} \quad \text { in } \Omega,\]
+
\[\nabla \cdot \mathbf{u} =0 \quad \text { in } \Omega,\]
+
\[\mathbf{u} =\mathbf{u}_{\Gamma} \quad \text { on } \Gamma_D,\]
+
\[\frac{\partial \mathbf{u}}{\partial n} =0 \quad \text { on } \Gamma_N.\]
+

2.1 Kovasznay flow(NSFNet1)

+

我们使用 Kovasznay 流作为第一个测试用例来演示 NSFnets 的性能。 该二维稳态纳维-斯托克斯流具有以下解析解:

+
\[u(x, y)=1-e^{\lambda x} \cos (2 \pi y),\]
+
\[v(x, y)=\frac{\lambda}{2 \pi} e^{\lambda x} \sin (2 \pi y),\]
+
\[p(x, y)=\frac{1}{2}\left(1-e^{2 \lambda x}\right),\]
+

其中

+
\[\lambda=\frac{1}{2 \nu}-\sqrt{\frac{1}{4 \nu^2}+4 \pi^2}, \quad \nu=\frac{1}{Re}=\frac{1}{40} .\]
+

我们考虑计算域为 \([−0.5, 1.0] × [−0.5, 1.5]\)。 我们首先确定优化策略。 每个边界上有 \(101\) 个具有固定空间坐标的点,即 \(Nb = 4 × 101\)。为了计算 NSFnet 的方程损失,在域内随机选择 \(2,601\) 个点。 这种稳定流动没有初始条件。 我们使用 Adam 优化器来提供一组更好的初始神经网络可学习变量。 然后,使用L-BFGS-B对神经网络进行微调以获得更高的精度。 L-BFGS-B的训练过程根据增量容差自动终止。 在本节中,我们在 L-BFGS-B 训练之前使用 \(3 × 10^4\) Adam 迭代,学习率为 \(10^{−3}\)。 Adam 迭代次数的影响在论文附录 A 的图 A.1 中讨论,我们还研究了 NSFnet 在采样点和边界点数量方面的性能。

+

2.2 Cylinder wake (NSFNet2)

+

这里我们使用 NSFnets 模拟 \(Re = 100\) 时圆柱体后面的 \(2D\) 涡旋脱落。圆柱体放置在 \((x, y) = (0, 0)\) 处,直径 \(D = 1\)。高保真 DNS 数据来自 \(M. Raissi 2019\) 用作参考并为 NSFnet 训练提供边界和初始数据。 我们考虑由 \([1, 8] × [−2, 2]\) 定义的域,时间间隔为 \([0, 7]\)(超过一个脱落周期),时间步长 \(Δt = 0.1\)。 对于训练数据,我们沿 \(x\) 方向边界放置 \(100\) 个点,沿 y 方向边界放置 \(50\) 个点来控制边界条件,并使用域内的 \(140,000\) 个时空分散点来计算残差。 NSFnet 包含 \(10\) 个隐藏层,每层有 \(100\) 个神经元。Cylinder wake AIstudio数据集链接

+

2.3 Beltrami flow (NSFNet3)

+
\[u(x, y, z, t)= -a\left[e^{a x} \sin (a y+d z)+e^{a z} \cos (a x+d y)\right] e^{-d^2 t}, \]
+
\[v(x, y, z, t)= -a\left[e^{a y} \sin (a z+d x)+e^{a x} \cos (a y+d z)\right] e^{-d^2 t}, \]
+
\[w(x, y, z, t)= -a\left[e^{a z} \sin (a x+d y)+e^{a y} \cos (a z+d x)\right] e^{-d^2 t}, \]
+
\[p(x, y, z, t)= -\frac{1}{2} a^2\left[e^{2 a x}+e^{2 a y}+e^{2 a z}+2 \sin (a x+d y) \cos (a z+d x) e^{a(y+z)} +2 \sin (a y+d z) \cos (a x+d y) e^{a(z+x)} +2 \sin (a z+d x) \cos (a y+d z) e^{a(x+y)}\right] e^{-2 d^2 t}.\]
+

3. 问题求解

+

3.1 模型构建

+

本文使用PINN经典的MLP模型进行训练。 +

model = ppsci.arch.MLP(**cfg.MODEL)
+
+

3.2 超参数设定

+

指定残差点、边界点、初值点的个数,以及可以指定边界损失函数和初值损失函数的权重 +

N_TRAIN = cfg.ntrain
+
+# set the number of boundary samples
+NB_TRAIN = cfg.nb_train
+
+# set the number of initial samples
+N0_TRAIN = cfg.n0_train
+ALPHA = cfg.alpha
+BETA = cfg.beta
+
+

3.3 数据生成

+

因数据集为解析解,我们先构造解析解函数 +

10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
def analytic_solution_generate(x, y, z, t):
+    a, d = 1, 1
+    u = (
+        -a
+        * (
+            np.exp(a * x) * np.sin(a * y + d * z)
+            + np.exp(a * z) * np.cos(a * x + d * y)
+        )
+        * np.exp(-d * d * t)
+    )
+    v = (
+        -a
+        * (
+            np.exp(a * y) * np.sin(a * z + d * x)
+            + np.exp(a * x) * np.cos(a * y + d * z)
+        )
+        * np.exp(-d * d * t)
+    )
+    w = (
+        -a
+        * (
+            np.exp(a * z) * np.sin(a * x + d * y)
+            + np.exp(a * y) * np.cos(a * z + d * x)
+        )
+        * np.exp(-d * d * t)
+    )
+    p = (
+        -0.5
+        * a
+        * a
+        * (
+            np.exp(2 * a * x)
+            + np.exp(2 * a * y)
+            + np.exp(2 * a * z)
+            + 2 * np.sin(a * x + d * y) * np.cos(a * z + d * x) * np.exp(a * (y + z))
+            + 2 * np.sin(a * y + d * z) * np.cos(a * x + d * y) * np.exp(a * (z + x))
+            + 2 * np.sin(a * z + d * x) * np.cos(a * y + d * z) * np.exp(a * (x + y))
+        )
+        * np.exp(-2 * d * d * t)
+    )
+
+    return u, v, w, p
+
+

然后先后取边界点、初值点、以及用于计算残差的内部点(具体取法见论文节3.3)以及生成测试点。 +

(
+    x_train,
+    y_train,
+    z_train,
+    t_train,
+    x0_train,
+    y0_train,
+    z0_train,
+    t0_train,
+    u0_train,
+    v0_train,
+    w0_train,
+    xb_train,
+    yb_train,
+    zb_train,
+    tb_train,
+    ub_train,
+    vb_train,
+    wb_train,
+    x_star,
+    y_star,
+    z_star,
+    t_star,
+    u_star,
+    v_star,
+    w_star,
+    p_star,
+) = generate_data(N_TRAIN)
+
+

3.4 约束构建

+

由于我们边界点和初值点具有解析解,因此我们使用监督约束 +

sup_constraint_b = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_b,
+    ppsci.loss.MSELoss("mean", ALPHA),
+    name="Sup_b",
+)
+
+# supervised constraint s.t ||u-u_0||
+sup_constraint_0 = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_0,
+    ppsci.loss.MSELoss("mean", BETA),
+    name="Sup_0",
+)
+
+

其中alpha和beta为该损失函数的权重,在本代码中与论文中描述一致,都取为100

+

使用内部点构造纳韦斯托克方程的残差约束 +

equation = {
+    "NavierStokes": ppsci.equation.NavierStokes(
+        nu=1.0 / cfg.re, rho=1.0, dim=3, time=True
+    ),
+}
+
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["NavierStokes"].equations,
+    {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
+    geom,
+    {
+        "dataset": {"name": "IterableNamedArrayDataset"},
+        "batch_size": N_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="EQ",
+)
+
+

3.5 评估器构建

+

使用在数据生成时生成的测试点构造的测试集用于模型评估: +

residual_validator = ppsci.validate.SupervisedValidator(
+    valida_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    output_expr={
+        "u": lambda d: d["u"],
+        "v": lambda d: d["v"],
+        "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+    },
+    metric={"L2R": ppsci.metric.L2Rel()},
+    name="Residual",
+)
+
+# wrap validator
+validator = {residual_validator.name: residual_validator}
+
+

3.6 优化器构建

+

与论文中描述相同,我们使用分段学习率构造Adam优化器,其中可以通过调节_epoch_list_来调节训练轮数。 +

# set optimizer
+epoch_list = [5000, 5000, 50000, 50000]
+new_epoch_list = []
+for i, _ in enumerate(epoch_list):
+    new_epoch_list.append(sum(epoch_list[: i + 1]))
+EPOCHS = new_epoch_list[-1]
+lr_list = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7]
+lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(
+    EPOCHS, ITERS_PER_EPOCH, new_epoch_list, lr_list
+)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.7 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model=model,
+    constraint=constraint,
+    optimizer=optimizer,
+    epochs=EPOCHS,
+    lr_scheduler=lr_scheduler,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    eval_during_train=True,
+    log_freq=cfg.log_freq,
+    eval_freq=cfg.eval_freq,
+    seed=SEED,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    visualizer=None,
+    eval_with_no_grad=False,
+)
+
+

最后启动训练即可:

+
# train model
+solver.train()
+
+

4. 完整代码

+

NSFNet1: +

NSFNet1.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def analytic_solution_generate(x, y, lam):
+    u = 1 - np.exp(lam * x) * np.cos(2 * np.pi * y)
+    v = lam / (2 * np.pi) * np.exp(lam * x) * np.sin(2 * np.pi * y)
+    p = 0.5 * (1 - np.exp(2 * lam * x))
+    return u, v, p
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="VP_NSFNet1.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+def generate_data(N_TRAIN, lam, seed):
+    x = np.linspace(-0.5, 1.0, 101)
+    y = np.linspace(-0.5, 1.5, 101)
+
+    yb1 = np.array([-0.5] * 100)
+    yb2 = np.array([1] * 100)
+    xb1 = np.array([-0.5] * 100)
+    xb2 = np.array([1.5] * 100)
+
+    y_train1 = np.concatenate([y[1:101], y[0:100], xb1, xb2], 0).astype("float32")
+    x_train1 = np.concatenate([yb1, yb2, x[0:100], x[1:101]], 0).astype("float32")
+
+    xb_train = x_train1.reshape(x_train1.shape[0], 1).astype("float32")
+    yb_train = y_train1.reshape(y_train1.shape[0], 1).astype("float32")
+    ub_train, vb_train, _ = analytic_solution_generate(xb_train, yb_train, lam)
+
+    x_train = (np.random.rand(N_TRAIN, 1) - 1 / 3) * 3 / 2
+    y_train = (np.random.rand(N_TRAIN, 1) - 1 / 4) * 2
+
+    # generate test data
+    np.random.seed(seed)
+    x_star = ((np.random.rand(1000, 1) - 1 / 3) * 3 / 2).astype("float32")
+    y_star = ((np.random.rand(1000, 1) - 1 / 4) * 2).astype("float32")
+
+    u_star, v_star, p_star = analytic_solution_generate(x_star, y_star, lam)
+
+    return (
+        x_train,
+        y_train,
+        xb_train,
+        yb_train,
+        ub_train,
+        vb_train,
+        x_star,
+        y_star,
+        u_star,
+        v_star,
+        p_star,
+    )
+
+
+def train(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+
+    ITERS_PER_EPOCH = cfg.iters_per_epoch
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    # set the number of boundary samples
+    NB_TRAIN = cfg.nb_train
+
+    # generate data
+
+    # set the Reynolds number and the corresponding lambda which is the parameter in the exact solution.
+    Re = cfg.re
+    lam = 0.5 * Re - np.sqrt(0.25 * (Re**2) + 4 * (np.pi**2))
+
+    (
+        x_train,
+        y_train,
+        xb_train,
+        yb_train,
+        ub_train,
+        vb_train,
+        x_star,
+        y_star,
+        u_star,
+        v_star,
+        p_star,
+    ) = generate_data(N_TRAIN, lam, SEED)
+
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": xb_train, "y": yb_train},
+            "label": {"u": ub_train, "v": vb_train},
+        },
+        "batch_size": NB_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star},
+            "label": {"u": u_star, "v": v_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    geom = ppsci.geometry.PointCloud({"x": x_train, "y": y_train}, ("x", "y"))
+
+    # supervised constraint s.t ||u-u_0||
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss("mean"),
+        name="Sup",
+    )
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / Re, rho=1.0, dim=2, time=False
+        ),
+    }
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom,
+        {
+            "dataset": {"name": "IterableNamedArrayDataset"},
+            "batch_size": N_TRAIN,
+            "iters_per_epoch": ITERS_PER_EPOCH,
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="EQ",
+    )
+
+    constraint = {
+        sup_constraint.name: sup_constraint,
+        pde_constraint.name: pde_constraint,
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # set learning rate scheduler
+    epoch_list = [5000, 5000, 50000, 50000]
+    new_epoch_list = []
+    for i, _ in enumerate(epoch_list):
+        new_epoch_list.append(sum(epoch_list[: i + 1]))
+    EPOCHS = new_epoch_list[-1]
+    lr_list = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7]
+
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(
+        EPOCHS, ITERS_PER_EPOCH, new_epoch_list, lr_list
+    )()
+
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/eval.log", "info")
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model=model,
+        constraint=constraint,
+        optimizer=optimizer,
+        epochs=EPOCHS,
+        lr_scheduler=lr_scheduler,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=False,
+        log_freq=cfg.log_freq,
+        eval_freq=cfg.eval_freq,
+        seed=SEED,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=None,
+        eval_with_no_grad=False,
+        output_dir=OUTPUT_DIR,
+    )
+
+    # train model
+    solver.train()
+
+    solver.eval()
+
+    # plot the loss
+    solver.plot_loss_history()
+
+    # set LBFGS optimizer
+    EPOCHS = 5000
+    optimizer = ppsci.optimizer.LBFGS(
+        max_iter=50000, tolerance_change=np.finfo(float).eps, history_size=50
+    )(model)
+
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/eval.log", "info")
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model=model,
+        constraint=constraint,
+        optimizer=optimizer,
+        epochs=EPOCHS,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=False,
+        log_freq=2000,
+        eval_freq=2000,
+        seed=SEED,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=None,
+        eval_with_no_grad=False,
+        output_dir=OUTPUT_DIR,
+    )
+    # train model
+    solver.train()
+
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+    ppsci.utils.load_pretrain(model, cfg.pretrained_model_path)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    # set the Reynolds number and the corresponding lambda which is the parameter in the exact solution.
+    Re = cfg.re
+    lam = 0.5 * Re - np.sqrt(0.25 * (Re**2) + 4 * (np.pi**2))
+
+    x_train = (np.random.rand(N_TRAIN, 1) - 1 / 3) * 3 / 2
+    y_train = (np.random.rand(N_TRAIN, 1) - 1 / 4) * 2
+
+    # generate test data
+    np.random.seed(SEED)
+    x_star = ((np.random.rand(1000, 1) - 1 / 3) * 3 / 2).astype("float32")
+    y_star = ((np.random.rand(1000, 1) - 1 / 4) * 2).astype("float32")
+    u_star = 1 - np.exp(lam * x_star) * np.cos(2 * np.pi * y_star)
+    v_star = (lam / (2 * np.pi)) * np.exp(lam * x_star) * np.sin(2 * np.pi * y_star)
+    p_star = 0.5 * (1 - np.exp(2 * lam * x_star))
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star},
+            "label": {"u": u_star, "v": v_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    geom = ppsci.geometry.PointCloud({"x": x_train, "y": y_train}, ("x", "y"))
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / Re, rho=1.0, dim=2, time=False
+        ),
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        output_expr={
+            "u": lambda d: d["u"],
+            "v": lambda d: d["v"],
+            "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+        },
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # load solver
+    solver = ppsci.solver.Solver(
+        model,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+
+    # eval model
+    solver.eval()
+
+
+if __name__ == "__main__":
+    main()
+
+NSFNet2: +
NSFNet2.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+import scipy
+from omegaconf import DictConfig
+from scipy.interpolate import griddata
+
+import ppsci
+from ppsci.utils import logger
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="VP_NSFNet2.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+def load_data(path, N_TRAIN, NB_TRAIN, N0_TRAIN):
+    data = scipy.io.loadmat(path)
+
+    U_star = data["U_star"].astype("float32")  # N x 2 x T
+    P_star = data["p_star"].astype("float32")  # N x T
+    t_star = data["t"].astype("float32")  # T x 1
+    X_star = data["X_star"].astype("float32")  # N x 2
+
+    N = X_star.shape[0]
+    T = t_star.shape[0]
+
+    # rearrange data
+    XX = np.tile(X_star[:, 0:1], (1, T))  # N x T
+    YY = np.tile(X_star[:, 1:2], (1, T))  # N x T
+    TT = np.tile(t_star, (1, N)).T  # N x T
+
+    UU = U_star[:, 0, :]  # N x T
+    VV = U_star[:, 1, :]  # N x T
+    PP = P_star  # N x T
+
+    x = XX.flatten()[:, None]  # NT x 1
+    y = YY.flatten()[:, None]  # NT x 1
+    t = TT.flatten()[:, None]  # NT x 1
+
+    u = UU.flatten()[:, None]  # NT x 1
+    v = VV.flatten()[:, None]  # NT x 1
+    p = PP.flatten()[:, None]  # NT x 1
+
+    data1 = np.concatenate([x, y, t, u, v, p], 1)
+    data2 = data1[:, :][data1[:, 2] <= 7]
+    data3 = data2[:, :][data2[:, 0] >= 1]
+    data4 = data3[:, :][data3[:, 0] <= 8]
+    data5 = data4[:, :][data4[:, 1] >= -2]
+    data_domain = data5[:, :][data5[:, 1] <= 2]
+    data_t0 = data_domain[:, :][data_domain[:, 2] == 0]
+    data_y1 = data_domain[:, :][data_domain[:, 0] == 1]
+    data_y8 = data_domain[:, :][data_domain[:, 0] == 8]
+    data_x = data_domain[:, :][data_domain[:, 1] == -2]
+    data_x2 = data_domain[:, :][data_domain[:, 1] == 2]
+    data_sup_b_train = np.concatenate([data_y1, data_y8, data_x, data_x2], 0)
+    idx = np.random.choice(data_domain.shape[0], N_TRAIN, replace=False)
+
+    x_train = data_domain[idx, 0].reshape(data_domain[idx, 0].shape[0], 1)
+    y_train = data_domain[idx, 1].reshape(data_domain[idx, 1].shape[0], 1)
+    t_train = data_domain[idx, 2].reshape(data_domain[idx, 2].shape[0], 1)
+
+    x0_train = data_t0[:, 0].reshape(data_t0[:, 0].shape[0], 1)
+    y0_train = data_t0[:, 1].reshape(data_t0[:, 1].shape[0], 1)
+    t0_train = data_t0[:, 2].reshape(data_t0[:, 2].shape[0], 1)
+    u0_train = data_t0[:, 3].reshape(data_t0[:, 3].shape[0], 1)
+    v0_train = data_t0[:, 4].reshape(data_t0[:, 4].shape[0], 1)
+
+    xb_train = data_sup_b_train[:, 0].reshape(data_sup_b_train[:, 0].shape[0], 1)
+    yb_train = data_sup_b_train[:, 1].reshape(data_sup_b_train[:, 1].shape[0], 1)
+    tb_train = data_sup_b_train[:, 2].reshape(data_sup_b_train[:, 2].shape[0], 1)
+    ub_train = data_sup_b_train[:, 3].reshape(data_sup_b_train[:, 3].shape[0], 1)
+    vb_train = data_sup_b_train[:, 4].reshape(data_sup_b_train[:, 4].shape[0], 1)
+
+    # set test set
+    snap = np.array([0])
+    x_star = X_star[:, 0:1]
+    y_star = X_star[:, 1:2]
+    t_star = TT[:, snap]
+
+    u_star = U_star[:, 0, snap]
+    v_star = U_star[:, 1, snap]
+    p_star = P_star[:, snap]
+
+    return (
+        x_train,
+        y_train,
+        t_train,
+        x0_train,
+        y0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        xb_train,
+        yb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        x_star,
+        y_star,
+        t_star,
+        u_star,
+        v_star,
+        p_star,
+    )
+
+
+def train(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+    ITERS_PER_EPOCH = cfg.iters_per_epoch
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    # set the number of boundary samples
+    NB_TRAIN = cfg.nb_train
+
+    # set the number of initial samples
+    N0_TRAIN = cfg.n0_train
+
+    (
+        x_train,
+        y_train,
+        t_train,
+        x0_train,
+        y0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        xb_train,
+        yb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        x_star,
+        y_star,
+        t_star,
+        u_star,
+        v_star,
+        p_star,
+    ) = load_data(cfg.data_dir, N_TRAIN, NB_TRAIN, N0_TRAIN)
+    # set dataloader config
+    train_dataloader_cfg_b = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": xb_train, "y": yb_train, "t": tb_train},
+            "label": {"u": ub_train, "v": vb_train},
+        },
+        "batch_size": NB_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    train_dataloader_cfg_0 = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x0_train, "y": y0_train, "t": t0_train},
+            "label": {"u": u0_train, "v": v0_train},
+        },
+        "batch_size": N0_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star, "t": t_star},
+            "label": {"u": u_star, "v": v_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    geom = ppsci.geometry.PointCloud(
+        {"x": x_train, "y": y_train, "t": t_train}, ("x", "y", "t")
+    )
+
+    # supervised constraint s.t ||u-u_b||
+    sup_constraint_b = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_b,
+        ppsci.loss.MSELoss("mean"),
+        name="Sup_b",
+    )
+
+    # supervised constraint s.t ||u-u_0||
+    sup_constraint_0 = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_0,
+        ppsci.loss.MSELoss("mean"),
+        name="Sup_0",
+    )
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / cfg.re, rho=1.0, dim=2, time=True
+        ),
+    }
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
+        geom,
+        {
+            "dataset": {"name": "IterableNamedArrayDataset"},
+            "batch_size": N_TRAIN,
+            "iters_per_epoch": ITERS_PER_EPOCH,
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="EQ",
+    )
+
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        sup_constraint_b.name: sup_constraint_b,
+        sup_constraint_0.name: sup_constraint_0,
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        output_expr={
+            "u": lambda d: d["u"],
+            "v": lambda d: d["v"],
+            "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+        },
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # set optimizer
+    epoch_list = [5000, 5000, 50000, 50000]
+    new_epoch_list = []
+    for i, _ in enumerate(epoch_list):
+        new_epoch_list.append(sum(epoch_list[: i + 1]))
+    EPOCHS = new_epoch_list[-1]
+    lr_list = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7]
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(
+        EPOCHS, ITERS_PER_EPOCH, new_epoch_list, lr_list
+    )()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/eval.log", "info")
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model=model,
+        constraint=constraint,
+        optimizer=optimizer,
+        epochs=EPOCHS,
+        lr_scheduler=lr_scheduler,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=True,
+        log_freq=cfg.log_freq,
+        eval_freq=cfg.eval_freq,
+        seed=SEED,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=None,
+        eval_with_no_grad=False,
+    )
+    # train model
+    solver.train()
+
+    # evaluate after finished training
+    solver.eval()
+
+    solver.plot_loss_history()
+
+
+def evaluate(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+    ppsci.utils.load_pretrain(model, cfg.pretrained_model_path)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    data = scipy.io.loadmat(cfg.data_dir)
+
+    U_star = data["U_star"].astype("float32")  # N x 2 x T
+    P_star = data["p_star"].astype("float32")  # N x T
+    t_star = data["t"].astype("float32")  # T x 1
+    X_star = data["X_star"].astype("float32")  # N x 2
+
+    N = X_star.shape[0]
+    T = t_star.shape[0]
+
+    # rearrange data
+    XX = np.tile(X_star[:, 0:1], (1, T))  # N x T
+    YY = np.tile(X_star[:, 1:2], (1, T))  # N x T
+    TT = np.tile(t_star, (1, N)).T  # N x T
+
+    UU = U_star[:, 0, :]  # N x T
+    VV = U_star[:, 1, :]  # N x T
+    PP = P_star  # N x T
+
+    x = XX.flatten()[:, None]  # NT x 1
+    y = YY.flatten()[:, None]  # NT x 1
+    t = TT.flatten()[:, None]  # NT x 1
+
+    u = UU.flatten()[:, None]  # NT x 1
+    v = VV.flatten()[:, None]  # NT x 1
+    p = PP.flatten()[:, None]  # NT x 1
+
+    data1 = np.concatenate([x, y, t, u, v, p], 1)
+    data2 = data1[:, :][data1[:, 2] <= 7]
+    data3 = data2[:, :][data2[:, 0] >= 1]
+    data4 = data3[:, :][data3[:, 0] <= 8]
+    data5 = data4[:, :][data4[:, 1] >= -2]
+    data_domain = data5[:, :][data5[:, 1] <= 2]
+
+    idx = np.random.choice(data_domain.shape[0], N_TRAIN, replace=False)
+
+    x_train = data_domain[idx, 0].reshape(data_domain[idx, 0].shape[0], 1)
+    y_train = data_domain[idx, 1].reshape(data_domain[idx, 1].shape[0], 1)
+    t_train = data_domain[idx, 2].reshape(data_domain[idx, 2].shape[0], 1)
+
+    snap = np.array([0])
+    x_star = X_star[:, 0:1]
+    y_star = X_star[:, 1:2]
+    t_star = TT[:, snap]
+
+    u_star = U_star[:, 0, snap]
+    v_star = U_star[:, 1, snap]
+    p_star = P_star[:, snap]
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star, "t": t_star},
+            "label": {"u": u_star, "v": v_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    geom = ppsci.geometry.PointCloud(
+        {"x": x_train, "y": y_train, "t": t_train}, ("x", "y", "t")
+    )
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(nu=0.01, rho=1.0, dim=2, time=True),
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        output_expr={
+            "u": lambda d: d["u"],
+            "v": lambda d: d["v"],
+            "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+        },
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+
+    # eval
+    ## eval validate set
+    solver.eval()
+
+    ## eval every time
+    us = []
+    vs = []
+    for i in range(0, 70):
+        snap = np.array([i])
+        x_star = X_star[:, 0:1]
+        y_star = X_star[:, 1:2]
+        t_star = TT[:, snap]
+        u_star = paddle.to_tensor(U_star[:, 0, snap])
+        v_star = paddle.to_tensor(U_star[:, 1, snap])
+        p_star = paddle.to_tensor(P_star[:, snap])
+
+        solution = solver.predict({"x": x_star, "y": y_star, "t": t_star})
+        u_pred = solution["u"]
+        v_pred = solution["v"]
+        p_pred = solution["p"]
+        p_pred = p_pred - p_pred.mean() + p_star.mean()
+        error_u = np.linalg.norm(u_star - u_pred, 2) / np.linalg.norm(u_star, 2)
+        error_v = np.linalg.norm(v_star - v_pred, 2) / np.linalg.norm(v_star, 2)
+        error_p = np.linalg.norm(p_star - p_pred, 2) / np.linalg.norm(p_star, 2)
+        us.append(error_u)
+        vs.append(error_v)
+        print("t={:.2f},relative error of u: {:.3e}".format(t_star[0].item(), error_u))
+        print("t={:.2f},relative error of v: {:.3e}".format(t_star[0].item(), error_v))
+        print("t={:.2f},relative error of p: {:.3e}".format(t_star[0].item(), error_p))
+
+    # plot
+    ## vorticity
+    grid_x, grid_y = np.mgrid[1.0:8.0:1000j, -2.0:2.0:1000j]
+    x_star = paddle.to_tensor(grid_x.reshape(-1, 1).astype("float32"))
+    y_star = paddle.to_tensor(grid_y.reshape(-1, 1).astype("float32"))
+    t_star = paddle.to_tensor((4.0) * np.ones(x_star.shape).astype("float32"))
+    x_star.stop_gradient = False
+    y_star.stop_gradient = False
+    t_star.stop_gradient = False
+    sol = model.forward({"x": x_star, "y": y_star, "t": t_star})
+    u_y = paddle.grad(sol["u"], y_star)
+    v_x = paddle.grad(sol["v"], x_star)
+    w = np.array(v_x) - np.array(u_y)
+    w = w.reshape(1000, 1000)
+    l1 = np.arange(-4, 0, 0.25)
+    l2 = np.arange(0.25, 4, 0.25)
+    fig = plt.figure(figsize=(16, 8), dpi=80)
+    plt.contour(grid_x, grid_y, w, levels=np.concatenate([l1, l2]), cmap="jet")
+    plt.savefig(f"{OUTPUT_DIR}/vorticity_t=4.png")
+
+    ## relative error
+    t_snap = []
+    for i in range(70):
+        t_snap.append(i / 10)
+    fig, ax = plt.subplots(1, 2, figsize=(12, 3))
+    ax[0].plot(t_snap, us)
+    ax[1].plot(t_snap, vs)
+    ax[0].set_title("u")
+    ax[1].set_title("v")
+    fig.savefig(f"{OUTPUT_DIR}/l2_error.png")
+
+    ## velocity
+    grid_x, grid_y = np.mgrid[0.0:8.0:1000j, -2.0:2.0:1000j]
+    for i in range(70):
+        snap = np.array([i])
+        x_star = X_star[:, 0:1]
+        y_star = X_star[:, 1:2]
+        t_star = TT[:, snap]
+        points = np.concatenate([x_star, y_star], -1)
+        u_star = U_star[:, 0, snap]
+        v_star = U_star[:, 1, snap]
+
+        solution = solver.predict({"x": x_star, "y": y_star, "t": t_star})
+        u_pred = solution["u"]
+        v_pred = solution["v"]
+        u_star_ = griddata(points, u_star, (grid_x, grid_y), method="cubic")
+        u_pred_ = griddata(points, u_pred, (grid_x, grid_y), method="cubic")
+        v_star_ = griddata(points, v_star, (grid_x, grid_y), method="cubic")
+        v_pred_ = griddata(points, v_pred, (grid_x, grid_y), method="cubic")
+        fig, ax = plt.subplots(2, 2, figsize=(12, 8))
+        ax[0, 0].contourf(grid_x, grid_y, u_star_[:, :, 0])
+        ax[0, 1].contourf(grid_x, grid_y, u_pred_[:, :, 0])
+        ax[1, 0].contourf(grid_x, grid_y, v_star_[:, :, 0])
+        ax[1, 1].contourf(grid_x, grid_y, v_pred_[:, :, 0])
+        ax[0, 0].set_title("u_exact")
+        ax[0, 1].set_title("u_pred")
+        ax[1, 0].set_title("v_exact")
+        ax[1, 1].set_title("v_pred")
+        fig.savefig(OUTPUT_DIR + f"/velocity_t={t_star[i]}.png")
+
+
+if __name__ == "__main__":
+    main()
+
+NSFNet3: +
NSFNet3.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def analytic_solution_generate(x, y, z, t):
+    a, d = 1, 1
+    u = (
+        -a
+        * (
+            np.exp(a * x) * np.sin(a * y + d * z)
+            + np.exp(a * z) * np.cos(a * x + d * y)
+        )
+        * np.exp(-d * d * t)
+    )
+    v = (
+        -a
+        * (
+            np.exp(a * y) * np.sin(a * z + d * x)
+            + np.exp(a * x) * np.cos(a * y + d * z)
+        )
+        * np.exp(-d * d * t)
+    )
+    w = (
+        -a
+        * (
+            np.exp(a * z) * np.sin(a * x + d * y)
+            + np.exp(a * y) * np.cos(a * z + d * x)
+        )
+        * np.exp(-d * d * t)
+    )
+    p = (
+        -0.5
+        * a
+        * a
+        * (
+            np.exp(2 * a * x)
+            + np.exp(2 * a * y)
+            + np.exp(2 * a * z)
+            + 2 * np.sin(a * x + d * y) * np.cos(a * z + d * x) * np.exp(a * (y + z))
+            + 2 * np.sin(a * y + d * z) * np.cos(a * x + d * y) * np.exp(a * (z + x))
+            + 2 * np.sin(a * z + d * x) * np.cos(a * y + d * z) * np.exp(a * (x + y))
+        )
+        * np.exp(-2 * d * d * t)
+    )
+
+    return u, v, w, p
+
+
+def generate_data(N_TRAIN):
+    # generate boundary data
+    x1 = np.linspace(-1, 1, 31)
+    y1 = np.linspace(-1, 1, 31)
+    z1 = np.linspace(-1, 1, 31)
+    t1 = np.linspace(0, 1, 11)
+    b0 = np.array([-1] * 900)
+    b1 = np.array([1] * 900)
+
+    xt = np.tile(x1[0:30], 30)
+    yt = np.tile(y1[0:30], 30)
+    xt1 = np.tile(x1[1:31], 30)
+    yt1 = np.tile(y1[1:31], 30)
+
+    yr = y1[0:30].repeat(30)
+    zr = z1[0:30].repeat(30)
+    yr1 = y1[1:31].repeat(30)
+    zr1 = z1[1:31].repeat(30)
+
+    train1x = np.concatenate([b1, b0, xt1, xt, xt1, xt], 0).repeat(t1.shape[0])
+    train1y = np.concatenate([yt, yt1, b1, b0, yr1, yr], 0).repeat(t1.shape[0])
+    train1z = np.concatenate([zr, zr1, zr, zr1, b1, b0], 0).repeat(t1.shape[0])
+    train1t = np.tile(t1, 5400)
+
+    train1ub, train1vb, train1wb, train1pb = analytic_solution_generate(
+        train1x, train1y, train1z, train1t
+    )
+
+    xb_train = train1x.reshape(train1x.shape[0], 1).astype("float32")
+    yb_train = train1y.reshape(train1y.shape[0], 1).astype("float32")
+    zb_train = train1z.reshape(train1z.shape[0], 1).astype("float32")
+    tb_train = train1t.reshape(train1t.shape[0], 1).astype("float32")
+    ub_train = train1ub.reshape(train1ub.shape[0], 1).astype("float32")
+    vb_train = train1vb.reshape(train1vb.shape[0], 1).astype("float32")
+    wb_train = train1wb.reshape(train1wb.shape[0], 1).astype("float32")
+
+    # generate initial data
+    x_0 = np.tile(x1, 31 * 31)
+    y_0 = np.tile(y1.repeat(31), 31)
+    z_0 = z1.repeat(31 * 31)
+    t_0 = np.array([0] * x_0.shape[0])
+    u_0, v_0, w_0, p_0 = analytic_solution_generate(x_0, y_0, z_0, t_0)
+    u0_train = u_0.reshape(u_0.shape[0], 1).astype("float32")
+    v0_train = v_0.reshape(v_0.shape[0], 1).astype("float32")
+    w0_train = w_0.reshape(w_0.shape[0], 1).astype("float32")
+    x0_train = x_0.reshape(x_0.shape[0], 1).astype("float32")
+    y0_train = y_0.reshape(y_0.shape[0], 1).astype("float32")
+    z0_train = z_0.reshape(z_0.shape[0], 1).astype("float32")
+    t0_train = t_0.reshape(t_0.shape[0], 1).astype("float32")
+
+    # unsupervised part
+    xx = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    yy = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    zz = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    tt = np.random.randint(11, size=N_TRAIN) / 10
+
+    x_train = xx.reshape(xx.shape[0], 1).astype("float32")
+    y_train = yy.reshape(yy.shape[0], 1).astype("float32")
+    z_train = zz.reshape(zz.shape[0], 1).astype("float32")
+    t_train = tt.reshape(tt.shape[0], 1).astype("float32")
+
+    # test data
+    x_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    y_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    z_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    t_star = (np.random.randint(11, size=(1000, 1)) / 10).astype("float32")
+
+    u_star, v_star, w_star, p_star = analytic_solution_generate(
+        x_star, y_star, z_star, t_star
+    )
+
+    return (
+        x_train,
+        y_train,
+        z_train,
+        t_train,
+        x0_train,
+        y0_train,
+        z0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        w0_train,
+        xb_train,
+        yb_train,
+        zb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        wb_train,
+        x_star,
+        y_star,
+        z_star,
+        t_star,
+        u_star,
+        v_star,
+        w_star,
+        p_star,
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="VP_NSFNet3.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+def train(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+    ITERS_PER_EPOCH = cfg.iters_per_epoch
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    # set the number of boundary samples
+    NB_TRAIN = cfg.nb_train
+
+    # set the number of initial samples
+    N0_TRAIN = cfg.n0_train
+    ALPHA = cfg.alpha
+    BETA = cfg.beta
+    (
+        x_train,
+        y_train,
+        z_train,
+        t_train,
+        x0_train,
+        y0_train,
+        z0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        w0_train,
+        xb_train,
+        yb_train,
+        zb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        wb_train,
+        x_star,
+        y_star,
+        z_star,
+        t_star,
+        u_star,
+        v_star,
+        w_star,
+        p_star,
+    ) = generate_data(N_TRAIN)
+
+    # set dataloader config
+    train_dataloader_cfg_b = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": xb_train, "y": yb_train, "z": zb_train, "t": tb_train},
+            "label": {"u": ub_train, "v": vb_train, "w": wb_train},
+        },
+        "batch_size": NB_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    train_dataloader_cfg_0 = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x0_train, "y": y0_train, "z": z0_train, "t": t0_train},
+            "label": {"u": u0_train, "v": v0_train, "w": w0_train},
+        },
+        "batch_size": N0_TRAIN,
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star, "z": z_star, "t": t_star},
+            "label": {"u": u_star, "v": v_star, "w": w_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+    geom = ppsci.geometry.PointCloud(
+        {"x": x_train, "y": y_train, "z": z_train, "t": t_train}, ("x", "y", "z", "t")
+    )
+
+    # supervised constraint s.t ||u-u_b||
+    sup_constraint_b = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_b,
+        ppsci.loss.MSELoss("mean", ALPHA),
+        name="Sup_b",
+    )
+
+    # supervised constraint s.t ||u-u_0||
+    sup_constraint_0 = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_0,
+        ppsci.loss.MSELoss("mean", BETA),
+        name="Sup_0",
+    )
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / cfg.re, rho=1.0, dim=3, time=True
+        ),
+    }
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
+        geom,
+        {
+            "dataset": {"name": "IterableNamedArrayDataset"},
+            "batch_size": N_TRAIN,
+            "iters_per_epoch": ITERS_PER_EPOCH,
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="EQ",
+    )
+
+    # wrap constraint
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        sup_constraint_b.name: sup_constraint_b,
+        sup_constraint_0.name: sup_constraint_0,
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        output_expr={
+            "u": lambda d: d["u"],
+            "v": lambda d: d["v"],
+            "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+        },
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # set optimizer
+    epoch_list = [5000, 5000, 50000, 50000]
+    new_epoch_list = []
+    for i, _ in enumerate(epoch_list):
+        new_epoch_list.append(sum(epoch_list[: i + 1]))
+    EPOCHS = new_epoch_list[-1]
+    lr_list = [1e-3, 1e-4, 1e-5, 1e-6, 1e-7]
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(
+        EPOCHS, ITERS_PER_EPOCH, new_epoch_list, lr_list
+    )()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/eval.log", "info")
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model=model,
+        constraint=constraint,
+        optimizer=optimizer,
+        epochs=EPOCHS,
+        lr_scheduler=lr_scheduler,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=True,
+        log_freq=cfg.log_freq,
+        eval_freq=cfg.eval_freq,
+        seed=SEED,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        visualizer=None,
+        eval_with_no_grad=False,
+    )
+    # train model
+    solver.train()
+
+    # evaluate after finished training
+    solver.eval()
+    solver.plot_loss_history()
+
+
+def evaluate(cfg: DictConfig):
+    OUTPUT_DIR = cfg.output_dir
+    logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+    # set random seed for reproducibility
+    SEED = cfg.seed
+    ppsci.utils.misc.set_random_seed(SEED)
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+    ppsci.utils.load_pretrain(model, cfg.pretrained_model_path)
+
+    # set the number of residual samples
+    N_TRAIN = cfg.ntrain
+
+    # unsupervised part
+    xx = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    yy = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    zz = np.random.randint(31, size=N_TRAIN) / 15 - 1
+    tt = np.random.randint(11, size=N_TRAIN) / 10
+
+    x_train = xx.reshape(xx.shape[0], 1).astype("float32")
+    y_train = yy.reshape(yy.shape[0], 1).astype("float32")
+    z_train = zz.reshape(zz.shape[0], 1).astype("float32")
+    t_train = tt.reshape(tt.shape[0], 1).astype("float32")
+
+    # test data
+    x_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    y_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    z_star = ((np.random.rand(1000, 1) - 1 / 2) * 2).astype("float32")
+    t_star = (np.random.randint(11, size=(1000, 1)) / 10).astype("float32")
+
+    u_star, v_star, w_star, p_star = analytic_solution_generate(
+        x_star, y_star, z_star, t_star
+    )
+
+    valida_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star, "z": z_star, "t": t_star},
+            "label": {"u": u_star, "v": v_star, "w": w_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    }
+    geom = ppsci.geometry.PointCloud(
+        {"x": x_train, "y": y_train, "z": z_train, "t": t_train}, ("x", "y", "z", "t")
+    )
+
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / cfg.re, rho=1.0, dim=3, time=True
+        ),
+    }
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valida_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        output_expr={
+            "u": lambda d: d["u"],
+            "v": lambda d: d["v"],
+            "p": lambda d: d["p"] - d["p"].min() + p_star.min(),
+        },
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # load solver
+    solver = ppsci.solver.Solver(
+        model,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+    )
+
+    # print the relative error
+    us = []
+    vs = []
+    ws = []
+    for i in [0, 0.25, 0.5, 0.75, 1.0]:
+        x_star, y_star, z_star = np.mgrid[-1.0:1.0:100j, -1.0:1.0:100j, -1.0:1.0:100j]
+        x_star, y_star, z_star = (
+            x_star.reshape(-1, 1),
+            y_star.reshape(-1, 1),
+            z_star.reshape(-1, 1),
+        )
+        t_star = i * np.ones(x_star.shape)
+        u_star, v_star, w_star, p_star = analytic_solution_generate(
+            x_star, y_star, z_star, t_star
+        )
+
+        solution = solver.predict({"x": x_star, "y": y_star, "z": z_star, "t": t_star})
+        u_pred = solution["u"]
+        v_pred = solution["v"]
+        w_pred = solution["w"]
+        p_pred = solution["p"]
+        p_pred = p_pred - p_pred.mean() + p_star.mean()
+        error_u = np.linalg.norm(u_star - u_pred, 2) / np.linalg.norm(u_star, 2)
+        error_v = np.linalg.norm(v_star - v_pred, 2) / np.linalg.norm(v_star, 2)
+        error_w = np.linalg.norm(w_star - w_pred, 2) / np.linalg.norm(w_star, 2)
+        error_p = np.linalg.norm(p_star - p_pred, 2) / np.linalg.norm(p_star, 2)
+        us.append(error_u)
+        vs.append(error_v)
+        ws.append(error_w)
+        print("t={:.2f},relative error of u: {:.3e}".format(t_star[0].item(), error_u))
+        print("t={:.2f},relative error of v: {:.3e}".format(t_star[0].item(), error_v))
+        print("t={:.2f},relative error of w: {:.3e}".format(t_star[0].item(), error_w))
+        print("t={:.2f},relative error of p: {:.3e}".format(t_star[0].item(), error_p))
+
+    ## plot vorticity
+    grid_x, grid_y = np.mgrid[-1.0:1.0:1000j, -1.0:1.0:1000j]
+    grid_x = grid_x.reshape(-1, 1)
+    grid_y = grid_y.reshape(-1, 1)
+    grid_z = np.zeros(grid_x.shape)
+    T = np.linspace(0, 1, 101)
+    for i in T:
+        t_star = i * np.ones(x_star.shape)
+        u_star, v_star, w_star, p_star = analytic_solution_generate(
+            grid_x, grid_y, grid_z, t_star
+        )
+
+        solution = solver.predict({"x": grid_x, "y": grid_y, "z": grid_z, "t": t_star})
+        u_pred = np.array(solution["u"])
+        v_pred = np.array(solution["v"])
+        w_pred = np.array(solution["w"])
+        p_pred = p_pred - p_pred.mean() + p_star.mean()
+        fig, ax = plt.subplots(3, 2, figsize=(12, 12))
+        ax[0, 0].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            u_star.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[0, 1].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            u_pred.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[1, 0].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            v_star.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[1, 1].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            v_pred.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[2, 0].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            w_star.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[2, 1].contourf(
+            grid_x.reshape(1000, 1000),
+            grid_y.reshape(1000, 1000),
+            w_pred.reshape(1000, 1000),
+            cmap=plt.get_cmap("RdYlBu"),
+        )
+        ax[0, 0].set_title("u_exact")
+        ax[0, 1].set_title("u_pred")
+        ax[1, 0].set_title("v_exact")
+        ax[1, 1].set_title("v_pred")
+        ax[2, 0].set_title("w_exact")
+        ax[2, 1].set_title("w_pred")
+        time = "%.3f" % i
+        fig.savefig(OUTPUT_DIR + f"/velocity_t={str(time)}.png")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

主要参考论文数据,和参考代码的数据。

+

5.1 NSFNet1(Kovasznay flow)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
velocitypapercodePaddleScienceNN size
u0.072%0.080%0.056%4 × 50
v0.058%0.539%0.399%4 × 50
p0.027%0.722%1.123%4 × 50
+

如表格所示,第2,3,4列分别为论文,其他开发者和PaddleScience复现的\(L_{2}\)误差Kovasznay flow在\(x\), \(y\)方向的速度\(u\), \(v\)\(L_{2}\)误差为0.055%和0.399%, 指标均优于论文(Table 2)和参考代码。

+

5.2 NSFNet2(Cylinder wake)

+

Cylinder wake在\(t=0\)时刻预测的\(L_{2}\)误差, 如表格所示, Cylinder flow在\(x\), \(y\)方向的速度\(u\), \(v\)\(L_{2}\)误差为0.138%和0.488%, 指标接近论文(Figure 9)和代码。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
velocitypaper (VP-NSFnet, \(\alpha=\beta=1\))paper (VP-NSFnet, dynamic weights)codePaddleScienceNN size
u0.09%0.01%0.403%0.138%4 × 50
v0.25%0.05%1.5%0.488%4 × 50
p1.9%0.8%//4 × 50
+

NSFNet2(2D Cylinder Flow)案例的速度场如下图所示, 第一行的两张图片为圆柱尾部绕流区域, 第一行的图片表示在\(x\)流线方向上的流速\(u\)的数值分布, 左侧为DNS高保真数据作为参考, 右侧为神经网络预测值, 蓝色为较小值, 绿色为较大值, 分布区域为 \(x=[1,8]\), \(y=[-2, 2]\), 第二行的图片表示在\(y\)展向方向上的流速\(v\)的分布,左侧为DNS高保真数据参考值, 右侧为神经网络预测值, 分布区域为 \(x=[1,8]\), \(y=[-2, 2]\)

+

image

+

根据速度场,我们可以计算涡流场, 如图所示, 为NSFNet2(2D Cylinder Flow)案例在\(t=4.0\)时刻的涡流场的等值线图, 我们根据\(x\), \(y\)方向的流速\(u\), \(v\),通过涡量计算公式, 计算得到如图所示涡量图, 涡结构连续性好, 和论文一致, 计算分布区域为\(x=[1, 8]\), \(y=[-2, 2]\)

+

image

+

5.3 NSFNet3(Beltrami flow)

+

测试数据集(解析解)相对误差如表格所示, Beltrami flow在\(x\), \(y\), \(z\)方向的速度\(u\), \(v\), \(w\)\(L_{2}\)误差为0.059%, 0.082%和0.0732%, 优于代码数据。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
velocitycode(NN size:10×100)PaddleScience (NN size:10×100)
u0.0766%0.059%
v0.0689%0.082%
w0.1090%0.073%
p//
+

Beltrami flow在 $ t=1 $ 时刻, $ z=0 \(平面上的预测相对误差, 如表格所示, Beltrami flow在\)x, y, z\(方向的速度\)u, v, w\(的\)L_{2}\(误差为0.115%, 0.199%和0.217%, 压力\)p\(的\)L_{2}$误差为0.1.986%, 均优于论文数据(Table 4. VP)。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
velocitypaper(NN size:7×50)PaddleScience(NN size:10×100)
u0.1634±0.0418%0.115%
v0.2185±0.0530%0.199%
w0.1783±0.0300%0.217%
p8.9335±2.4350%1.986%
+

Beltrami flow速度场,如图所示,左侧为解析解参考值,右侧为神经网络预测值,蓝色为较小值,红色为较大值,分布区域为\(x=[-1,1]\), \(y=[-1, 1]\), 第一行为在\(x\)方向上的流速\(u\)的分布,第二行为在\(y\)方向上的流速\(v\)的分布,第三行为在\(z\)方向上流速\(w\)的分布。

+

image

+

6. 结果说明

+

我们使用PINN对不可压纳韦斯托克方程进行数值求解。在PINN中,随机选取的时间和空间的坐标被当作输入值,所对应的速度场以及压强场被当作输出值,使用初值、边界条件当作监督约束以及纳韦斯托克方程本身的当作无监督约束条件加入损失函数进行训练。我们针对三个不同类型的PINN纳韦斯托克方程, 设计了三个不同的流体案例, 即NSFNet1、NSFNet2、NSFNet3。通过损失函数的下降、网络预测结果与高保真DNS数据,以及解析解的\(L_{2}\)误差的降低,可以证明神经网络在求解纳韦斯托克方程中的收敛性, 表明NSFNets的架构拥有对不可压纳韦斯托克方程的求解能力。而通过实验结果表明, 三个使用NSFNet的正问题案例,都可以很好的逼近参考解, 并且我们发现增加边界约束, 以及初值约束的权重可以使得神经网络拥有更好的逼近效果。

+

7. 参考资料

+

NSFnets (Navier-Stokes Flow nets): Physics-informed neural networks for the incompressible Navier-Stokes equations

+

Github NSFnets

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/nsfnet4/index.html b/zh/examples/nsfnet4/index.html new file mode 100644 index 0000000000..392cea9fde --- /dev/null +++ b/zh/examples/nsfnet4/index.html @@ -0,0 +1,5180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSFNet4 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

NSFNet4

+

AI Studio快速体验

+
+
+
+
# VP_NSFNet4
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip -P ./data/
+unzip ./data/NSF4_data.zip
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip --create-dirs -o ./data/NSF4_data.zip
+# unzip ./data/NSF4_data.zip
+python VP_NSFNet4.py    mode=eval  data_dir=./data/  EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/nsfnet/nsfnet4.pdparams
+
+
+
+
# VP_NSFNet4
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip -P ./data/
+unzip ./data/NSF4_data.zip
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip --create-dirs -o ./data/NSF4_data.zip
+# unzip ./data/NSF4_data.zip
+python VP_NSFNet4.py data_dir=./data/
+
+
+
+
python VP_NSFNet4.py mode=export
+
+
+
+
# VP_NSFNet4
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip -P ./data/
+unzip ./data/NSF4_data.zip
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/NSFNet/NSF4_data.zip --create-dirs -o ./data/NSF4_data.zip
+# unzip ./data/NSF4_data.zip
+python VP_NSFNet4.py mode=infer
+
+
+
+
+

1. 背景简介

+

最近几年, 深度学习在很多领域取得了非凡的成就, 尤其是计算机视觉和自然语言处理方面, 而受启发于深度学习的快速发展, 基于深度学习强大的函数逼近能力, 神经网络在科学计算领域也取得了成功, 现阶段的研究主要分为两大类, 一类是将物理信息以及物理限制加入损失函数来对神经网络进行训练, 其代表有 PINN 以及 Deep Ritz Net, 另一类是通过数据驱动的深度神经网络算子, 其代表有 FNO 以及 DeepONet。这些方法都在科学实践中获得了广泛应用, 比如天气预测, 量子化学, 生物工程, 以及计算流体等领域。而为充分探索PINN对流体方程的求解能力, 本次复现论文作者设计了NSFNets, 并且先后使用具有解析解或数值解的二维、三维纳韦斯托克方程以及使用DNS方法进行高精度求解的数据集作为参考, 进行正问题求解训练。论文实验表明PINN对不可压纳韦斯托克方程具有优秀的数值求解能力, 本项目主要目标是使用PaddleScience复现论文所实现的高精度求解纳韦斯托克方程的代码。

+

2. 问题定义

+

本问题所使用的为最经典的PINN模型, 对此不再赘述。

+

主要介绍所求解的几类纳韦斯托克方程:

+

不可压纳韦斯托克方程可以表示为:

+
\[\frac{\partial \mathbf{u}}{\partial t}+(\mathbf{u} \cdot \nabla) \mathbf{u} =-\nabla p+\frac{1}{Re} \nabla^2 \mathbf{u} \quad \text { in } \Omega, \]
+
\[\nabla \cdot \mathbf{u} =0 \quad \text { in } \Omega, \]
+
\[\mathbf{u} =\mathbf{u}_{\Gamma} \quad \text { on } \Gamma_D, \]
+
\[\frac{\partial \mathbf{u}}{\partial n} =0 \quad \text { on } \Gamma_N.\]
+

2.1 JHTDB 数据集

+

数据集为使用DNS求解Re=999.35的三维不可压强迫各向同性湍流的高精度数据集, 详细参数可见readme.

+

3. 问题求解

+

3.1 模型构建

+

本文使用PINN经典的MLP模型进行训练。

+
model = ppsci.arch.MLP(**cfg.MODEL)
+
+

3.2 数据生成

+

先后取边界点、初值点、以及用于计算残差的内部点(具体取法见论文节3.3)以及生成测试点。

+
# load data
+(
+    x_train,
+    y_train,
+    z_train,
+    t_train,
+    x0_train,
+    y0_train,
+    z0_train,
+    t0_train,
+    u0_train,
+    v0_train,
+    w0_train,
+    xb_train,
+    yb_train,
+    zb_train,
+    tb_train,
+    ub_train,
+    vb_train,
+    wb_train,
+    x_star,
+    y_star,
+    z_star,
+    t_star,
+    u_star,
+    v_star,
+    w_star,
+    p_star,
+) = generate_data(cfg.data_dir)
+
+

3.3 归一化处理

+

为将所取较小长方体区域改为正方体区域, 我们将归一化函数嵌入网络训练前。

+
# normalization
+Xb = np.concatenate([xb_train, yb_train, zb_train, tb_train], 1)
+lowb = Xb.min(0)  # minimal number in each column
+upb = Xb.max(0)
+trans = Transform(paddle.to_tensor(lowb), paddle.to_tensor(upb))
+model.register_input_transform(trans.input_trans)
+
+

3.4 约束构建

+

由于我们边界点和初值点具有解析解, 因此我们使用监督约束, 其中alpha和beta为该损失函数的权重, 在本代码中与论文中描述一致, 都取为100。

+
sup_constraint_b = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_b,
+    ppsci.loss.MSELoss("mean", cfg.alpha),
+    name="Sup_b",
+)
+
+# supervised constraint s.t ||u-u_0||
+sup_constraint_0 = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg_ic,
+    ppsci.loss.MSELoss("mean", cfg.beta),
+    name="Sup_ic",
+)
+
+

使用内部点构造纳韦斯托克方程的残差约束

+
# set equation constarint s.t. ||F(u)||
+equation = {
+    "NavierStokes": ppsci.equation.NavierStokes(
+        nu=1.0 / cfg.re, rho=1.0, dim=3, time=True
+    ),
+}
+
+pde_constraint = ppsci.constraint.InteriorConstraint(
+    equation["NavierStokes"].equations,
+    {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
+    geom,
+    {
+        "dataset": {"name": "NamedArrayDataset"},
+        "batch_size": cfg.ntrain,
+        "iters_per_epoch": cfg.TRAIN.lr_scheduler.iters_per_epoch,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    name="EQ",
+)
+
+

3.5 评估器构建

+

使用在数据生成时生成的测试点构造的测试集用于模型评估:

+
residual_validator = ppsci.validate.SupervisedValidator(
+    valid_dataloader_cfg,
+    ppsci.loss.L2RelLoss(),
+    metric={"L2R": ppsci.metric.L2Rel()},
+    name="Residual",
+)
+
+

3.6 优化器构建

+

与论文中描述相同, 我们使用分段学习率构造Adam优化器, 其中可以通过调节epoch_list来调节训练轮数。

+
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(**cfg.TRAIN.lr_scheduler)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+
+

3.7 模型训练与评估

+

完成上述设置之后, 只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model=model,
+    constraint=constraint,
+    output_dir=cfg.output_dir,
+    optimizer=optimizer,
+    lr_scheduler=lr_scheduler,
+    epochs=cfg.epochs,
+    iters_per_epoch=cfg.TRAIN.lr_scheduler.iters_per_epoch,
+    log_freq=cfg.TRAIN.log_freq,
+    save_freq=cfg.TRAIN.save_freq,
+    eval_freq=cfg.TRAIN.eval_freq,
+    eval_during_train=True,
+    seed=cfg.seed,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+)
+
+

最后启动训练即可:

+
# train model
+solver.train()
+
+

4. 完整代码

+
NSFNet.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path as osp
+
+import hydra
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def generate_data(data_dir):
+    train_ini1 = np.load(osp.join(data_dir, "train_ini2.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    train_iniv1 = np.load(osp.join(data_dir, "train_iniv2.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    train_xb1 = np.load(osp.join(data_dir, "train_xb2.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    train_vb1 = np.load(osp.join(data_dir, "train_vb2.npy")).astype(
+        paddle.get_default_dtype()
+    )
+
+    xnode = np.linspace(12.47, 12.66, 191).astype(paddle.get_default_dtype())
+    ynode = np.linspace(-1, -0.0031, 998).astype(paddle.get_default_dtype())
+    znode = np.linspace(4.61, 4.82, 211).astype(paddle.get_default_dtype())
+
+    x0_train = train_ini1[:, 0:1]
+    y0_train = train_ini1[:, 1:2]
+    z0_train = train_ini1[:, 2:3]
+    t0_train = np.zeros_like(train_ini1[:, 0:1]).astype(paddle.get_default_dtype())
+    u0_train = train_iniv1[:, 0:1]
+    v0_train = train_iniv1[:, 1:2]
+    w0_train = train_iniv1[:, 2:3]
+
+    xb_train = train_xb1[:, 0:1]
+    yb_train = train_xb1[:, 1:2]
+    zb_train = train_xb1[:, 2:3]
+    tb_train = train_xb1[:, 3:4]
+    ub_train = train_vb1[:, 0:1]
+    vb_train = train_vb1[:, 1:2]
+    wb_train = train_vb1[:, 2:3]
+
+    x_train1 = xnode.reshape(-1, 1)[np.random.choice(191, 100000, replace=True), :]
+    y_train1 = ynode.reshape(-1, 1)[np.random.choice(998, 100000, replace=True), :]
+    z_train1 = znode.reshape(-1, 1)[np.random.choice(211, 100000, replace=True), :]
+    x_train = np.tile(x_train1, (17, 1))
+    y_train = np.tile(y_train1, (17, 1))
+    z_train = np.tile(z_train1, (17, 1))
+
+    total_times1 = (np.array(list(range(17))) * 0.0065).astype(
+        paddle.get_default_dtype()
+    )
+    t_train1 = total_times1.repeat(100000)
+    t_train = t_train1.reshape(-1, 1)
+    # test data
+    test_x = np.load(osp.join(data_dir, "test43_l.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    test_v = np.load(osp.join(data_dir, "test43_vp.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    t = np.array([0.0065, 4 * 0.0065, 7 * 0.0065, 10 * 0.0065, 13 * 0.0065]).astype(
+        paddle.get_default_dtype()
+    )
+    t_star = np.tile(t.reshape(5, 1), (1, 3000)).reshape(-1, 1)
+    x_star = np.tile(test_x[:, 0:1], (5, 1))
+    y_star = np.tile(test_x[:, 1:2], (5, 1))
+    z_star = np.tile(test_x[:, 2:3], (5, 1))
+    u_star = test_v[:, 0:1]
+    v_star = test_v[:, 1:2]
+    w_star = test_v[:, 2:3]
+    p_star = test_v[:, 3:4]
+
+    return (
+        x_train,
+        y_train,
+        z_train,
+        t_train,
+        x0_train,
+        y0_train,
+        z0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        w0_train,
+        xb_train,
+        yb_train,
+        zb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        wb_train,
+        x_star,
+        y_star,
+        z_star,
+        t_star,
+        u_star,
+        v_star,
+        w_star,
+        p_star,
+    )
+
+
+class Transform:
+    def __init__(self, lowb, upb) -> None:
+        self.lowb = {"x": lowb[0], "y": lowb[1], "z": lowb[2], "t": lowb[3]}
+        self.upb = {"x": upb[0], "y": upb[1], "z": upb[2], "t": upb[3]}
+
+    def input_trans(self, input_dict):
+        for key, v in input_dict.items():
+            v = 2.0 * (v - self.lowb[key]) / (self.upb[key] - self.lowb[key]) - 1.0
+            input_dict[key] = v
+        return input_dict
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # load data
+    (
+        x_train,
+        y_train,
+        z_train,
+        t_train,
+        x0_train,
+        y0_train,
+        z0_train,
+        t0_train,
+        u0_train,
+        v0_train,
+        w0_train,
+        xb_train,
+        yb_train,
+        zb_train,
+        tb_train,
+        ub_train,
+        vb_train,
+        wb_train,
+        x_star,
+        y_star,
+        z_star,
+        t_star,
+        u_star,
+        v_star,
+        w_star,
+        p_star,
+    ) = generate_data(cfg.data_dir)
+
+    # normalization
+    Xb = np.concatenate([xb_train, yb_train, zb_train, tb_train], 1)
+    lowb = Xb.min(0)  # minimal number in each column
+    upb = Xb.max(0)
+    trans = Transform(paddle.to_tensor(lowb), paddle.to_tensor(upb))
+    model.register_input_transform(trans.input_trans)
+
+    # set dataloader config
+    train_dataloader_cfg_b = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": xb_train, "y": yb_train, "z": zb_train, "t": tb_train},
+            "label": {"u": ub_train, "v": vb_train, "w": wb_train},
+        },
+        "batch_size": cfg.nb_train,
+        "iters_per_epoch": cfg.TRAIN.lr_scheduler.iters_per_epoch,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    }
+
+    train_dataloader_cfg_ic = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x0_train, "y": y0_train, "z": z0_train, "t": t0_train},
+            "label": {"u": u0_train, "v": v0_train, "w": w0_train},
+        },
+        "batch_size": cfg.n0_train,
+        "iters_per_epoch": cfg.TRAIN.lr_scheduler.iters_per_epoch,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    }
+
+    valid_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"x": x_star, "y": y_star, "z": z_star, "t": t_star},
+            "label": {"u": u_star, "v": v_star, "w": w_star, "p": p_star},
+        },
+        "total_size": u_star.shape[0],
+        "batch_size": u_star.shape[0],
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    }
+
+    geom = ppsci.geometry.PointCloud(
+        {"x": x_train, "y": y_train, "z": z_train, "t": t_train}, ("x", "y", "z", "t")
+    )
+    # supervised constraint s.t ||u-u_b||
+    sup_constraint_b = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_b,
+        ppsci.loss.MSELoss("mean", cfg.alpha),
+        name="Sup_b",
+    )
+
+    # supervised constraint s.t ||u-u_0||
+    sup_constraint_0 = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg_ic,
+        ppsci.loss.MSELoss("mean", cfg.beta),
+        name="Sup_ic",
+    )
+
+    # set equation constarint s.t. ||F(u)||
+    equation = {
+        "NavierStokes": ppsci.equation.NavierStokes(
+            nu=1.0 / cfg.re, rho=1.0, dim=3, time=True
+        ),
+    }
+
+    pde_constraint = ppsci.constraint.InteriorConstraint(
+        equation["NavierStokes"].equations,
+        {"continuity": 0, "momentum_x": 0, "momentum_y": 0, "momentum_z": 0},
+        geom,
+        {
+            "dataset": {"name": "NamedArrayDataset"},
+            "batch_size": cfg.ntrain,
+            "iters_per_epoch": cfg.TRAIN.lr_scheduler.iters_per_epoch,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        name="EQ",
+    )
+
+    # wrap constraints
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        sup_constraint_b.name: sup_constraint_b,
+        sup_constraint_0.name: sup_constraint_0,
+    }
+
+    residual_validator = ppsci.validate.SupervisedValidator(
+        valid_dataloader_cfg,
+        ppsci.loss.L2RelLoss(),
+        metric={"L2R": ppsci.metric.L2Rel()},
+        name="Residual",
+    )
+
+    # wrap validator
+    validator = {residual_validator.name: residual_validator}
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Piecewise(**cfg.TRAIN.lr_scheduler)()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)(model)
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model=model,
+        constraint=constraint,
+        output_dir=cfg.output_dir,
+        optimizer=optimizer,
+        lr_scheduler=lr_scheduler,
+        epochs=cfg.epochs,
+        iters_per_epoch=cfg.TRAIN.lr_scheduler.iters_per_epoch,
+        log_freq=cfg.TRAIN.log_freq,
+        save_freq=cfg.TRAIN.save_freq,
+        eval_freq=cfg.TRAIN.eval_freq,
+        eval_during_train=True,
+        seed=cfg.seed,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+
+    # evaluate after finished training
+    solver.eval()
+
+    solver.plot_loss_history()
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # test Data
+    test_x = np.load(osp.join(cfg.data_dir, "test43_l.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    test_v = np.load(osp.join(cfg.data_dir, "test43_vp.npy")).astype(
+        paddle.get_default_dtype()
+    )
+    t = np.array([0.0065, 4 * 0.0065, 7 * 0.0065, 10 * 0.0065, 13 * 0.0065]).astype(
+        paddle.get_default_dtype()
+    )
+    t_star = paddle.to_tensor(np.tile(t.reshape(5, 1), (1, 3000)).reshape(-1, 1))
+    x_star = paddle.to_tensor(np.tile(test_x[:, 0:1], (5, 1)).reshape(-1, 1))
+    y_star = paddle.to_tensor(np.tile(test_x[:, 1:2], (5, 1)).reshape(-1, 1))
+    z_star = paddle.to_tensor(np.tile(test_x[:, 2:3], (5, 1)).reshape(-1, 1))
+    u_star = paddle.to_tensor(test_v[:, 0:1])
+    v_star = paddle.to_tensor(test_v[:, 1:2])
+    w_star = paddle.to_tensor(test_v[:, 2:3])
+    p_star = paddle.to_tensor(test_v[:, 3:4])
+
+    # wrap validator
+    ppsci.utils.load_pretrain(model, cfg.EVAL.pretrained_model_path)
+
+    # print the relative error
+    solution = model(
+        {
+            "x": x_star,
+            "y": y_star,
+            "z": z_star,
+            "t": t_star,
+        }
+    )
+    u_pred = solution["u"].reshape((5, -1))
+    v_pred = solution["v"].reshape((5, -1))
+    w_pred = solution["w"].reshape((5, -1))
+    p_pred = solution["p"].reshape((5, -1))
+    u_star = u_star.reshape((5, -1))
+    v_star = v_star.reshape((5, -1))
+    w_star = w_star.reshape((5, -1))
+    p_star = p_star.reshape((5, -1))
+
+    # NS equation can figure out pressure drop, need background pressure p_star.mean()
+    p_pred = p_pred - p_pred.mean() + p_star.mean()
+
+    u_error = paddle.linalg.norm(u_pred - u_star, axis=1) / np.linalg.norm(
+        u_star, axis=1
+    )
+    v_error = paddle.linalg.norm(v_pred - v_star, axis=1) / np.linalg.norm(
+        v_star, axis=1
+    )
+    w_error = paddle.linalg.norm(w_pred - w_star, axis=1) / np.linalg.norm(
+        w_star, axis=1
+    )
+    p_error = paddle.linalg.norm(p_pred - p_star, axis=1) / np.linalg.norm(
+        w_star, axis=1
+    )
+    t = np.array([0.0065, 4 * 0.0065, 7 * 0.0065, 10 * 0.0065, 13 * 0.0065])
+    plt.plot(t, np.array(u_error))
+    plt.plot(t, np.array(v_error))
+    plt.plot(t, np.array(w_error))
+    plt.plot(t, np.array(p_error))
+    plt.legend(["u_error", "v_error", "w_error", "p_error"])
+    plt.xlabel("t")
+    plt.ylabel("Relative l2 Error")
+    plt.title("Relative l2 Error, on test dataset")
+    plt.savefig(osp.join(cfg.output_dir, "error.jpg"))
+    logger.info("L2 error picture is saved")
+
+    grid_x, grid_y = np.mgrid[
+        x_star.min() : x_star.max() : 100j, y_star.min() : y_star.max() : 100j
+    ].astype(paddle.get_default_dtype())
+    x_plot = paddle.to_tensor(grid_x.reshape(-1, 1))
+    y_plot = paddle.to_tensor(grid_y.reshape(-1, 1))
+    z_plot = paddle.to_tensor(z_star.min() * paddle.ones(y_plot.shape))
+    t_plot = paddle.to_tensor((t[-1]) * np.ones(x_plot.shape), paddle.float32)
+    sol = model({"x": x_plot, "y": y_plot, "z": z_plot, "t": t_plot})
+    fig, ax = plt.subplots(1, 4, figsize=(16, 4))
+    cmap = matplotlib.colormaps.get_cmap("jet")
+
+    ax[0].contourf(grid_x, grid_y, sol["u"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[0].set_title("u prediction")
+    ax[1].contourf(grid_x, grid_y, sol["v"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[1].set_title("v prediction")
+    ax[2].contourf(grid_x, grid_y, sol["w"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[2].set_title("w prediction")
+    ax[3].contourf(grid_x, grid_y, sol["p"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[3].set_title("p prediction")
+    norm = matplotlib.colors.Normalize(
+        vmin=sol["u"].min(), vmax=sol["u"].max()
+    )  # set maximum and minimum
+    im = plt.cm.ScalarMappable(norm=norm, cmap=cmap)
+    ax13 = fig.add_axes([0.125, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.325, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.525, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.725, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    plt.savefig(osp.join(cfg.output_dir, "z=0 plane"))
+
+    grid_y, grid_z = np.mgrid[
+        y_star.min() : y_star.max() : 100j, z_star.min() : z_star.max() : 100j
+    ].astype(paddle.get_default_dtype())
+    z_plot = paddle.to_tensor(grid_z.reshape(-1, 1))
+    y_plot = paddle.to_tensor(grid_y.reshape(-1, 1))
+    x_plot = paddle.to_tensor(x_star.min() * paddle.ones(y_plot.shape))
+    t_plot = paddle.to_tensor((t[-1]) * np.ones(x_plot.shape), paddle.float32)
+    sol = model({"x": x_plot, "y": y_plot, "z": z_plot, "t": t_plot})
+    fig, ax = plt.subplots(1, 4, figsize=(16, 4))
+    cmap = matplotlib.colormaps.get_cmap("jet")
+
+    ax[0].contourf(grid_y, grid_z, sol["u"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[0].set_title("u prediction")
+    ax[1].contourf(grid_y, grid_z, sol["v"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[1].set_title("v prediction")
+    ax[2].contourf(grid_y, grid_z, sol["w"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[2].set_title("w prediction")
+    ax[3].contourf(grid_y, grid_z, sol["p"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[3].set_title("p prediction")
+    norm = matplotlib.colors.Normalize(
+        vmin=sol["u"].min(), vmax=sol["u"].max()
+    )  # set maximum and minimum
+    im = plt.cm.ScalarMappable(norm=norm, cmap=cmap)
+    ax13 = fig.add_axes([0.125, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.325, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.525, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.725, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    plt.savefig(osp.join(cfg.output_dir, "x=0 plane"))
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set models
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # load pretrained model
+    solver = ppsci.solver.Solver(
+        model=model, pretrained_model_path=cfg.INFER.pretrained_model_path
+    )
+
+    # export models
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # infer Data
+    test_x = np.load(osp.join(cfg.data_dir, "test43_l.npy")).astype(np.float32)
+    test_v = np.load(osp.join(cfg.data_dir, "test43_vp.npy")).astype(np.float32)
+    t = np.array([0.0065, 4 * 0.0065, 7 * 0.0065, 10 * 0.0065, 13 * 0.0065]).astype(
+        np.float32
+    )
+    t_star = np.tile(t.reshape(5, 1), (1, 3000)).reshape(-1, 1)
+    x_star = np.tile(test_x[:, 0:1], (5, 1)).reshape(-1, 1)
+    y_star = np.tile(test_x[:, 1:2], (5, 1)).reshape(-1, 1)
+    z_star = np.tile(test_x[:, 2:3], (5, 1)).reshape(-1, 1)
+    u_star = test_v[:, 0:1]
+    v_star = test_v[:, 1:2]
+    w_star = test_v[:, 2:3]
+    p_star = test_v[:, 3:4]
+
+    pred = predictor.predict(
+        {
+            "x": x_star,
+            "y": y_star,
+            "z": z_star,
+            "t": t_star,
+        },
+        cfg.INFER.batch_size,
+    )
+
+    pred = {
+        store_key: pred[infer_key]
+        for store_key, infer_key in zip(cfg.INFER.output_keys, pred.keys())
+    }
+
+    u_pred = pred["u"].reshape((5, -1))
+    v_pred = pred["v"].reshape((5, -1))
+    w_pred = pred["w"].reshape((5, -1))
+    p_pred = pred["p"].reshape((5, -1))
+    u_star = u_star.reshape((5, -1))
+    v_star = v_star.reshape((5, -1))
+    w_star = w_star.reshape((5, -1))
+    p_star = p_star.reshape((5, -1))
+
+    # NS equation can figure out pressure drop, need background pressure p_star.mean()
+    p_pred = p_pred - p_pred.mean() + p_star.mean()
+
+    u_error = np.linalg.norm(u_pred - u_star, axis=1) / np.linalg.norm(u_star, axis=1)
+    v_error = np.linalg.norm(v_pred - v_star, axis=1) / np.linalg.norm(v_star, axis=1)
+    w_error = np.linalg.norm(w_pred - w_star, axis=1) / np.linalg.norm(w_star, axis=1)
+    p_error = np.linalg.norm(p_pred - p_star, axis=1) / np.linalg.norm(w_star, axis=1)
+    t = np.array([0.0065, 4 * 0.0065, 7 * 0.0065, 10 * 0.0065, 13 * 0.0065])
+    plt.plot(t, np.array(u_error))
+    plt.plot(t, np.array(v_error))
+    plt.plot(t, np.array(w_error))
+    plt.plot(t, np.array(p_error))
+    plt.legend(["u_error", "v_error", "w_error", "p_error"])
+    plt.xlabel("t")
+    plt.ylabel("Relative l2 Error")
+    plt.title("Relative l2 Error, on test dataset")
+    plt.savefig(osp.join(cfg.output_dir, "error.jpg"))
+
+    grid_x, grid_y = np.mgrid[
+        x_star.min() : x_star.max() : 100j, y_star.min() : y_star.max() : 100j
+    ].astype(np.float32)
+    x_plot = grid_x.reshape(-1, 1)
+    y_plot = grid_y.reshape(-1, 1)
+    z_plot = (z_star.min() * np.ones(y_plot.shape)).astype(np.float32)
+    t_plot = ((t[-1]) * np.ones(x_plot.shape)).astype(np.float32)
+    sol = predictor.predict(
+        {"x": x_plot, "y": y_plot, "z": z_plot, "t": t_plot}, cfg.INFER.batch_size
+    )
+    sol = {
+        store_key: sol[infer_key]
+        for store_key, infer_key in zip(cfg.INFER.output_keys, sol.keys())
+    }
+    fig, ax = plt.subplots(1, 4, figsize=(16, 4))
+    cmap = matplotlib.colormaps.get_cmap("jet")
+
+    ax[0].contourf(grid_x, grid_y, sol["u"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[0].set_title("u prediction")
+    ax[1].contourf(grid_x, grid_y, sol["v"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[1].set_title("v prediction")
+    ax[2].contourf(grid_x, grid_y, sol["w"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[2].set_title("w prediction")
+    ax[3].contourf(grid_x, grid_y, sol["p"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[3].set_title("p prediction")
+    norm = matplotlib.colors.Normalize(
+        vmin=sol["u"].min(), vmax=sol["u"].max()
+    )  # set maximum and minimum
+    im = plt.cm.ScalarMappable(norm=norm, cmap=cmap)
+    ax13 = fig.add_axes([0.125, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.325, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.525, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.725, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    plt.savefig(osp.join(cfg.output_dir, "z=0 plane"))
+
+    grid_y, grid_z = np.mgrid[
+        y_star.min() : y_star.max() : 100j, z_star.min() : z_star.max() : 100j
+    ].astype(np.float32)
+    z_plot = grid_z.reshape(-1, 1)
+    y_plot = grid_y.reshape(-1, 1)
+    x_plot = (x_star.min() * np.ones(y_plot.shape)).astype(np.float32)
+    t_plot = ((t[-1]) * np.ones(x_plot.shape)).astype(np.float32)
+    sol = predictor.predict(
+        {"x": x_plot, "y": y_plot, "z": z_plot, "t": t_plot}, cfg.INFER.batch_size
+    )
+    sol = {
+        store_key: sol[infer_key]
+        for store_key, infer_key in zip(cfg.INFER.output_keys, sol.keys())
+    }
+    fig, ax = plt.subplots(1, 4, figsize=(16, 4))
+    cmap = matplotlib.colormaps.get_cmap("jet")
+
+    ax[0].contourf(grid_y, grid_z, sol["u"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[0].set_title("u prediction")
+    ax[1].contourf(grid_y, grid_z, sol["v"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[1].set_title("v prediction")
+    ax[2].contourf(grid_y, grid_z, sol["w"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[2].set_title("w prediction")
+    ax[3].contourf(grid_y, grid_z, sol["p"].reshape(grid_x.shape), levels=50, cmap=cmap)
+    ax[3].set_title("p prediction")
+    norm = matplotlib.colors.Normalize(
+        vmin=sol["u"].min(), vmax=sol["u"].max()
+    )  # set maximum and minimum
+    im = plt.cm.ScalarMappable(norm=norm, cmap=cmap)
+    ax13 = fig.add_axes([0.125, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.325, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.525, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    ax13 = fig.add_axes([0.725, 0.0, 0.175, 0.02])
+    plt.colorbar(im, cax=ax13, orientation="horizontal")
+    plt.savefig(osp.join(cfg.output_dir, "x=0 plane"))
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="VP_NSFNet4.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

NSFNet4

+

如图所示, NSFNet的结果在时间上的误差相对平稳, 并未出现传统方法中经常出现的误差累积问题。其中, 虽然在训练过程中三个方向的速度并未被设置权重, 但是训练结果可以看出, 神经网络在第一个速度方向u上面逼近效果最好, 在第三个速度方向w上面逼近效果次之, 在第二个速度v上面逼近效果最差且出现较为明显的误差累积现象。 +image

+

如图所示, 在x=12.47的y-z平面的轮廓图, 第一个为速度u的轮廓图, 第二个为速度v的轮廓图, 第三个为速度w的轮廓图, 第四个为速度p的轮廓图。可以看出, 速度u的轮廓图相对于v, w, p来说较为光滑。 +image

+

如图所示, 在z=4.61的x-y平面的轮廓图, 第一个为速度u的轮廓图, 第二个为速度v的轮廓图, 第三个为速度w的轮廓图, 第四个为速度p的轮廓图。可以看出, 速度u的轮廓图相对于v, w, p来说较为光滑。 +image

+

综上所述, 虽然u, v, w三个速度方向都是需要神经网络进行训练, 但是对于JHTDB数据集来说, u方向数据较为光滑, 更容易被神经网络所学习。因此在后续研究中, 可以尝试对三个不同方向的分量分而治之, 加大复杂分量方向的训练强度, 减少简单分量方向的训练强度。

+

6. 结果说明

+

我们使用PINN对不可压纳韦斯托克方程进行数值求解。在PINN中, 随机选取的时间和空间的坐标被当作输入值, 所对应的速度场以及压强场被当作输出值, 使用初值、边界条件当作监督约束以及纳韦斯托克方程本身的当作无监督约束条件加入损失函数进行训练。我们使用高精度JHTDB数据集进行训练。通过损失函数的下降可以证明神经网络在求解纳韦斯托克方程中的收敛性, 表明PINN拥有对不可压强迫各项同性湍流的求解能力。而通过实验结果表明, PINN可以很好的逼近对应的高精度不可压强迫各项同性湍流数据集, 并且, 我们发现增加边界约束以及初值约束的权重可以使得神经网络拥有更好的逼近效果。相比之下, 在误差允许范围内, 使用PINN求解该纳韦斯托克方程比原本使用DNS方法的推理速度更快。

+

7. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/phycrnet/index.html b/zh/examples/phycrnet/index.html new file mode 100644 index 0000000000..4755f64a70 --- /dev/null +++ b/zh/examples/phycrnet/index.html @@ -0,0 +1,4216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PhyCRNet - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

PhyCRNet

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyCRNet/burgers_1501x2x128x128.mat -P ./data/
+
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyCRNet/burgers_1501x2x128x128.mat --create-dirs -o ./data/burgers_1501x2x128x128.mat
+
+python main.py DATA_PATH=./data/burgers_1501x2x128x128.mat
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyCRNet/burgers_1501x2x128x128.mat -P ./data/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyCRNet/burgers_1501x2x128x128.mat --create-dirs -o ./data/burgers_1501x2x128x128.mat
+
+python main.py mode=eval DATA_PATH=./data/burgers_1501x2x128x128.mat EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/phycrnet/phycrnet_burgers.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
phycrnet_burgers_pretrained.pdparamsa-RMSE: 3.20e-3
+

1. 背景简介

+

复杂时空系统通常可以通过偏微分方程(PDE)来建模,它们在许多领域都十分常见,如应用数学、物理学、生物学、化学和工程学。求解PDE系统一直是科学计算领域的一个关键组成部分。 +本文的具体目标是为了提出一种新颖的、考虑物理信息的卷积-递归学习架构(PhyCRNet)及其轻量级变体(PhyCRNet-s),用于解决没有任何标签数据的多维时间空间PDEs。本项目主要目标是使用PaddleScience复现论文所提供的代码,并与代码的精度对齐。

+

该网络有以下优势:

+

1、 使用ConvLSTM(enconder-decoder Convolutional Long Short-Term Memory network) 可以充分提取低维空间上的特征以及学习其时间上的变化。

+

2、使用一个全局的残差迭代从而可以严格地执行时间上的迭代过程。

+

3、使用基于高阶有限差分格式的滤波从而能够精确求解重要的偏微分方程导数值。

+

4、使用强制边界条件是的所求解的数值解可以满足原方程所要求的初值以及边界条件。

+

2. 问题定义

+

在本模型中,我们考虑的是含有时间和空间的PDE模型,此类模型在推理过程中会存在时间上的误差累积的问题,因此,本文通过设计循环卷积神经网络试图减轻每一步时间迭代的误差累积。而我们所求解的问题为以高斯分布随机得到的值为初值的二维Burgers' Equation:

+
\[u_t+u\cdot \nabla u -\nu u =0\]
+

二维Burgers' Equation 刻画了复杂的非线性的反应扩散相互作用的问题,因此,经常被用来当作benchmark来比较各种科学计算算法。

+

3. 问题求解

+

3.1 模型构建

+

在这一部分中,我们介绍 PhyCRNet 的架构,包括编码器-解码器模块、残差连接、自回归(AR)过程和基于过滤的微分。网络架构如图所示。编码器(黄色Encoder,包含3个卷积层),用于从输入状态变量 \(u(t=i),i = 0,1,2,..,T-1\) 学习低维潜在特征,其中 \(T\) 表示总时间步。我们应用 ReLU 作为卷积层的激活函数。然后,我们将ConvLSTM层的输出(Encoder得到的低分辨率),潜在特征的时间传播器(绿色部分),其中,输出的LSTM的记忆单元 \(C_i\) 和LSTM的隐藏变量单元 \(h_i\) 会作为下一个时间步的输入。这样做的好处是对低维变量的基本动态进行建模,能够准确地捕获时间依赖性,同时有助于减轻记忆负担。 使用 LSTM 的另一个优势来自输出状态的双曲正切函数,它可以保持平滑的梯度曲线,并将值控制在 -1 和 1 之间。在建立低分辨率LSTM卷积循环方案后,我们基于上采样操作Decoder(蓝色部分)直接将低分辨率潜在空间重建为高分辨率量。特别注明,应用了子像素卷积层(即像素shuffle),因为与反卷积相比,它具有更好的效率和重建精度,且伪像更少。 最后,我们添加另一个卷积层,用于将有界潜变量空间输出,缩放回原始的物理空间。该Decoder后面没有激活函数。 此外,值得一提的是,鉴于输入变量数量有限及其对超分辨率的缺陷,我们在 PhyCRNet 中没有考虑 batch normalization。 作为替代,我们使用 batch normalization 来训练网络,以实现训练加速和更好的收敛性。受到动力学中,Forward Eular Scheme 的启发,我们在输入状态变量 \(u_i\) 和输出变量 \(u_{i+1}\) 之间附加全局残差连接。具体网络结构如下图所示:

+

image

+

接下来,剩下的挑战是,如何进行物理嵌入,来融合N-S方程带来的精度提升。我们应用无梯度卷积滤波器,来表示离散数值微分,以近似感兴趣的导数项。 例如,我们在本文中考虑的基于 Finite Difference 有限差分的滤波器是2阶和4阶中心差分格式,来计算时间和空间导数。

+

时间差分:

+
\[K_t = [-1,0,1] \times \frac{1}{2 \delta t},\]
+

空间差分:

+
\[K_s = \begin{bmatrix} + 0 & 0 & -1 & 0 & 0 \\ + 0 & 0 & 16 & 0 & 0 \\ + -1 & 16 & -60 & 16 & -1 \\ + 0 & 0 & 16 & 0 & 0 \\ + 0 & 0 & -1 & 0 & 0 \\ +\end{bmatrix} \times \frac{1}{12 (\delta x)^2},\]
+

其中 \(\delta t\)\(\delta x\) 表示时间步长和空间步长。

+

此外需要注意无法直接计算边界上的导数,丢失边界差异信息的风险可以通过接下来引入的在传统有限差分中经常使用的鬼点填充机制来减轻,其主要核心是在矩阵外围填充一层或多层鬼点(层数取决于差分格式,即,过滤器的大小),以下图为例,在迪利克雷边界条件(Dirichlet BCs)下,我们只需要把常值鬼点在原矩阵外围填充即可;在诺伊曼边界条件(Neumann BCs)下,我们需要根据其边界条件导数值确定鬼点的值。

+

image

+
    dt=cfg.DT, step=steps, effective_step=effective_step, **cfg.MODEL
+)
+
+
34
+35
+36
+37
+38
+39
+40
+41
# model settings
+MODEL:
+  input_channels: 2
+  hidden_channels: [8, 32, 128, 128]
+  input_kernel_size: [4, 4, 4, 3]
+  input_stride: [2, 2, 2, 1]
+  input_padding: [1, 1, 1, 1]
+  num_layers: [3, 1]
+
+

3.2 数据载入

+

我们使用RK4或者谱方法生成的数据(初值为使用正态分布生成),需要从.mat文件中将其读入,:

+
54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
uv = data["uv"]  # [t,c,h,w]
+functions.uv = uv
+
+# generate input data
+(
+    input_dict_train,
+    label_dict_train,
+    input_dict_val,
+    label_dict_val,
+) = functions.Dataset(
+    paddle.to_tensor(initial_state),
+    paddle.to_tensor(
+        uv[
+            0:1,
+        ],
+        dtype=paddle.get_default_dtype(),
+    ),
+).get()
+
+

3.3 约束构建

+

设置约束以及相关损失函数:

+
74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_train,
+            "label": label_dict_train,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.train_loss_func),
+    {
+        "loss": lambda out: out["loss"],
+    },
+    name="sup_train",
+)
+constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+

3.4 评估器构建

+

设置评估数据集和相关损失函数:

+
    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_val,
+            "label": label_dict_val,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.val_loss_func),
+    {
+        "loss": lambda out: out["loss"],
+    },
+    metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+    name="sup_valid",
+)
+validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择 Adam 优化器并设定 learning_rate

+
optimizer = ppsci.optimizer.Adam(scheduler)(model)
+solver = ppsci.solver.Solver(
+    model,
+    constraint_pde,
+
+

3.7 模型训练与评估

+

为了评估所有基于神经网络的求解器产生的解决方案精度,我们分两个阶段评估了全场误差传播:训练和外推。在时刻 τ 的全场误差 \(\epsilon_\tau\) 的定义为给定 b 的累积均方根误差 (a-RMSE)。

+
\[ +\epsilon_\tau=\sqrt{\frac{1}{N_\tau} \sum_{k=1}^{N_\tau} \frac{\left\|\mathbf{u}^*\left(\mathbf{x}, t_k\right)-\mathbf{u}^\theta\left(\mathbf{x}, t_k\right)\right\|_2^2}{m n}} +\]
+

这一步需要通过设置外界函数来进行,因此在训练过程中,我们使用function.transform_out来进行训练

+
47
+48
+49
+50
    return functions.transform_out(_in, _out, model)
+
+model.register_input_transform(functions.transform_in)
+model.register_output_transform(_transform_out)
+
+

而在评估过程中,我们使用function.tranform_output_val来进行评估,并生成累计均方根误差。

+
(h0, c0) = (paddle.randn((1, 128, 16, 16)), paddle.randn((1, 128, 16, 16)))
+
+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
    cfg.output_dir,
+    optimizer,
+    scheduler,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    validator=validator_pde,
+    eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+
+

最后启动训练、评估即可:

+
def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set initial states for convlstm
+
+

4. 完整代码

+
phycrnet
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
"""
+PhyCRNet for solving spatiotemporal PDEs
+Reference: https://github.com/isds-neu/PhyCRNet/
+"""
+from os import path as osp
+
+import functions
+import hydra
+import paddle
+import scipy.io as scio
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set initial states for convlstm
+    NUM_CONVLSTM = cfg.num_convlstm
+    (h0, c0) = (paddle.randn((1, 128, 16, 16)), paddle.randn((1, 128, 16, 16)))
+    initial_state = []
+    for _ in range(NUM_CONVLSTM):
+        initial_state.append((h0, c0))
+
+    # grid parameters
+    time_steps = cfg.TIME_STEPS
+    dx = cfg.DX[0] / cfg.DX[1]
+
+    steps = cfg.TIME_BATCH_SIZE + 1
+    effective_step = list(range(0, steps))
+    num_time_batch = int(time_steps / cfg.TIME_BATCH_SIZE)
+
+    functions.dt = cfg.DT
+    functions.dx = dx
+    functions.time_steps = cfg.TIME_STEPS
+    functions.num_time_batch = num_time_batch
+    model = ppsci.arch.PhyCRNet(
+        dt=cfg.DT, step=steps, effective_step=effective_step, **cfg.MODEL
+    )
+
+    def _transform_out(_in, _out):
+        return functions.transform_out(_in, _out, model)
+
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(_transform_out)
+
+    # use Burgers_2d_solver_HighOrder.py to generate data
+    data = scio.loadmat(cfg.DATA_PATH)
+    uv = data["uv"]  # [t,c,h,w]
+    functions.uv = uv
+
+    # generate input data
+    (
+        input_dict_train,
+        label_dict_train,
+        input_dict_val,
+        label_dict_val,
+    ) = functions.Dataset(
+        paddle.to_tensor(initial_state),
+        paddle.to_tensor(
+            uv[
+                0:1,
+            ],
+            dtype=paddle.get_default_dtype(),
+        ),
+    ).get()
+
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_train,
+                "label": label_dict_train,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func),
+        {
+            "loss": lambda out: out["loss"],
+        },
+        name="sup_train",
+    )
+    constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.val_loss_func),
+        {
+            "loss": lambda out: out["loss"],
+        },
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    scheduler = ppsci.optimizer.lr_scheduler.Step(**cfg.TRAIN.lr_scheduler)()
+
+    optimizer = ppsci.optimizer.Adam(scheduler)(model)
+    solver = ppsci.solver.Solver(
+        model,
+        constraint_pde,
+        cfg.output_dir,
+        optimizer,
+        scheduler,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        validator=validator_pde,
+        eval_with_no_grad=cfg.TRAIN.eval_with_no_grad,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    model.register_output_transform(functions.tranform_output_val)
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # set initial states for convlstm
+    NUM_CONVLSTM = cfg.num_convlstm
+    (h0, c0) = (paddle.randn((1, 128, 16, 16)), paddle.randn((1, 128, 16, 16)))
+    initial_state = []
+    for _ in range(NUM_CONVLSTM):
+        initial_state.append((h0, c0))
+
+    # grid parameters
+    time_steps = cfg.TIME_STEPS
+    dx = cfg.DX[0] / cfg.DX[1]
+
+    steps = cfg.EVAL.TIME_BATCH_SIZE + 1
+    effective_step = list(range(0, steps))
+    num_time_batch = int(time_steps / cfg.EVAL.TIME_BATCH_SIZE)
+
+    functions.dt = cfg.DT
+    functions.dx = dx
+    functions.num_time_batch = num_time_batch
+    model = ppsci.arch.PhyCRNet(
+        dt=cfg.DT, step=steps, effective_step=effective_step, **cfg.MODEL
+    )
+
+    def _transform_out(_in, _out):
+        return functions.transform_out(_in, _out, model)
+
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(_transform_out)
+
+    # use the generated data
+    data = scio.loadmat(cfg.DATA_PATH)
+    uv = data["uv"]  # [t,c,h,w]
+    functions.uv = uv
+    _, _, input_dict_val, _ = functions.Dataset(
+        paddle.to_tensor(initial_state),
+        paddle.to_tensor(uv[0:1, ...], dtype=paddle.get_default_dtype()),
+    ).get()
+    ppsci.utils.load_pretrain(model, cfg.EVAL.pretrained_model_path)
+    model.register_output_transform(None)
+    functions.output_graph(model, input_dict_val, cfg.output_dir, cfg.case_name)
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="burgers_equations.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

本文通过对Burgers' Equation进行训练,所得结果如下,根据精度和扩展能力的对比我们可以得出,我们的模型在训练集(t=1.0,2.0)以及拓展集(t=3.0,4.0)上均有良好的表现效果。pred为使用网络预测的速度的第一分量u在定义域上的contour图,truth为真实的速度第一分量u在定义域上的contour图,Error为预测值与真实值之间在整个定义域差值。

+

image

+

6. 结果说明

+

求解偏微分方程是在科学计算中的一个基本问题,而神经网络求解偏微分方程在求解逆问题以及数据同化问题等在传统方法上具有挑战性的问题上具有显著效果,但是,现有神经网络求解方法受限制于可扩展性,误差传导以及泛化能力等问题。因此,本论文通过提出一个新的神经网络PhyCRNet,通过将传统有限差分的思路嵌入物理信息神经网络中,针对性地解决原神经网络缺少对长时间数据的推理能力、误差累积以及缺少泛化能力的问题。与此同时,本文通过类似于有限差分的边界处理方式,将原本边界条件的软限制转为硬限制,大大提高了神经网络的准确性。新提出的网络可以有效解决上述提到的数据同化问题以及逆问题。

+

7. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/phygeonet/index.html b/zh/examples/phygeonet/index.html new file mode 100644 index 0000000000..6b129aa05c --- /dev/null +++ b/zh/examples/phygeonet/index.html @@ -0,0 +1,4281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PhyGeoNet - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

PhyGeoNet

+

AI Studio快速体验

+
+
+
+
# heat_equation
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation.npz -P ./data/
+
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation.npz --create-dirs -o ./data/heat_equation.npz
+
+python heat_equation.py
+
+# heat_equation_bc
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc.npz -P ./data/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc_test.npz -P ./data/
+
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc.npz --create-dirs -o ./data/heat_equation.npz
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc_test.npz --create-dirs -o ./data/heat_equation.npz
+
+python heat_equation_with_bc.py
+
+
+
+
# heat_equation
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation.npz -P ./data/
+
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation.npz --create-dirs -o ./data/heat_equation.npz
+
+python heat_equation.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/PhyGeoNet/heat_equation_pretrain.pdparams
+
+# heat_equation_bc
+# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc.npz -P ./data/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc_test.npz -P ./data/
+
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc.npz --create-dirs -o ./data/heat_equation.npz
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyGeoNet/heat_equation_bc_test.npz --create-dirs -o ./data/heat_equation.npz
+
+python heat_equation_with_bc.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/PhyGeoNet/heat_equation_bc_pretrain.pdparams
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
模型mResev
heat_equation_pretrain.pdparams0.8150.095
heat_equation_bc_pretrain.pdparams9920.31
+

1. 背景简介

+

最近几年,深度学习在很多领域取得了非凡的成就,尤其是计算机视觉和自然语言处理方面,受启发于深度学习的快速发展,基于深度学习强大的函数逼近能力,神经网络在科学计算领域也取得了成功,现阶段的研究主要分为两大类,一类是将物理信息以及物理限制加入损失函数来对神经网络进行训练, 其代表有 PINN 以及 Deep Ritz Net,另一类是通过数据驱动的深度神经网络算子,其代表有 FNO 以及 DeepONet。这些方法都在科学实践中获得了广泛应用,比如天气预测,量子化学,生物工程,以及计算流体等领域。由于卷积神经网络具有参数共享的性质,可以学习大尺度的时空域,因此获得了越来越多的关注。

+

2. 问题定义

+

而在实际科学计算问题中,很多偏微分方程的求解域是复杂边界且非均匀的。现有神经网络往往针对具有规则边界以及均匀网格的求解域,所以并没有实际应用效果。

+

本文针对物理信息神经网络在复杂边界非均匀网格求解域上效果较差的问题,提出了通过坐标变化将不规则边界非均匀网格变成规则边界均匀网格的方法,除此之外,本文利用变成均匀网格后,卷积神经网络的上述优势,提出相对应的的物理信息卷积神经网络。

+

3. 问题求解

+

为节约篇幅,接下来将以 heat equation 为例讲解如何使用 PaddleScience 进行实现。

+

3.1 模型构建

+

本案例使用提出的 USCNN 模型进行训练,该模型的构建入方式如下所示。

+
model = ppsci.arch.USCNN(**cfg.MODEL)
+
+

其中,构建模型所需的参数可以从对应的配置文件中获取。

+
34
+35
+36
+37
+38
+39
+40
+41
+42
+43
MODEL:
+  input_keys: [ 'coords' ]
+  output_keys: [ 'output_v' ]
+  hidden_size: [16, 32, 16]
+  h: 0.01
+  ny: 19
+  nx: 84
+  nvar_in: 2
+  nvar_out: 1
+  pad_singleside: 1
+
+

3.2 数据读取

+

本案例使用的数据集存储在 .npz 文件中,使用如下的代码进行读取。

+
15
+16
+17
+18
+19
+20
+21
data = np.load(cfg.data_dir)
+coords = data["coords"]
+jinvs = data["jinvs"]
+dxdxis = data["dxdxis"]
+dydxis = data["dydxis"]
+dxdetas = data["dxdetas"]
+dydetas = data["dydetas"]
+
+

3.3 输出转化函数构建

+

本文为强制边界约束,在训练时使用相对应的输出转化函数对模型的输出结果计算微分。

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
sup_constraint = {sup_constraint_res.name: sup_constraint_res}
+
+def _transform_out(
+    _input: Dict[str, paddle.Tensor],
+    _output: Dict[str, paddle.Tensor],
+    pad_singleside: int = cfg.MODEL.pad_singleside,
+):
+    """Calculation residual.
+
+    Args:
+        _input (Dict[str, paddle.Tensor]): The input of the model.
+        _output (Dict[str, paddle.Tensor]): The output of the model.
+        pad_singleside (int, optional): Pad size. Defaults to cfg.MODEL.pad_singleside.
+    """
+    output_v = _output["output_v"]
+    jinv = _input["jinvs"]
+    dxdxi = _input["dxdxis"]
+    dydxi = _input["dydxis"]
+    dxdeta = _input["dxdetas"]
+    dydeta = _input["dydetas"]
+    output_v[:, 0, -pad_singleside:, pad_singleside:-pad_singleside] = 0
+    output_v[:, 0, :pad_singleside, pad_singleside:-pad_singleside] = 1
+    output_v[:, 0, pad_singleside:-pad_singleside, -pad_singleside:] = 1
+    output_v[:, 0, pad_singleside:-pad_singleside, 0:pad_singleside] = 1
+    output_v[:, 0, 0, 0] = 0.5 * (output_v[:, 0, 0, 1] + output_v[:, 0, 1, 0])
+    output_v[:, 0, 0, -1] = 0.5 * (output_v[:, 0, 0, -2] + output_v[:, 0, 1, -1])
+    dvdx = utils.dfdx(output_v, dydeta, dydxi, jinv)
+    d2vdx2 = utils.dfdx(dvdx, dydeta, dydxi, jinv)
+    dvdy = utils.dfdy(output_v, dxdxi, dxdeta, jinv)
+    d2vdy2 = utils.dfdy(dvdy, dxdxi, dxdeta, jinv)
+
+

3.4 约束构建

+

构建相对应约束条件,由于边界约束为强制约束,约束条件主要为残差约束。

+
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
sup_constraint_res = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {
+                "coords": coords,
+                "jinvs": jinvs,
+                "dxdxis": dxdxis,
+                "dydxis": dydxis,
+                "dxdetas": dxdetas,
+                "dydetas": dydetas,
+            },
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "iters_per_epoch": iters_per_epoch,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(
+        lambda out, label, weight: {"residual": out["residual"]}
+    ),
+    name="residual",
+
+

3.5 优化器构建

+

与论文中描述相同,我们使用恒定学习率 0.001 构造 Adam 优化器。

+
optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.6 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
82
+83
+84
+85
+86
+87
+88
model.register_output_transform(_transform_out)
+solver = ppsci.solver.Solver(
+    model,
+    sup_constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.epochs,
+
+

最后启动训练即可:

+
iters_per_epoch=iters_per_epoch,
+
+

3.7 模型评估

+

在模型训练完成之后,可以使用 evaluate() 函数对训练好的模型进行评估,并可视化。

+
def evaluate(cfg: DictConfig):
+    data = np.load(cfg.data_dir)
+    coords = data["coords"]
+
+    ofv_sb = paddle.to_tensor(data["OFV_sb"])
+
+    ## create model
+    pad_singleside = cfg.MODEL.pad_singleside
+    model = ppsci.arch.USCNN(**cfg.MODEL)
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,  ### the path of the model
+    )
+    output_v = solver.predict({"coords": paddle.to_tensor(coords)})
+    output_v = output_v["output_v"]
+
+    output_v[0, 0, -pad_singleside:, pad_singleside:-pad_singleside] = 0
+    output_v[0, 0, :pad_singleside, pad_singleside:-pad_singleside] = 1
+    output_v[0, 0, pad_singleside:-pad_singleside, -pad_singleside:] = 1
+    output_v[0, 0, pad_singleside:-pad_singleside, 0:pad_singleside] = 1
+    output_v[0, 0, 0, 0] = 0.5 * (output_v[0, 0, 0, 1] + output_v[0, 0, 1, 0])
+    output_v[0, 0, 0, -1] = 0.5 * (output_v[0, 0, 0, -2] + output_v[0, 0, 1, -1])
+
+    ev = paddle.sqrt(
+        paddle.mean((ofv_sb - output_v[0, 0]) ** 2) / paddle.mean(ofv_sb**2)
+    ).item()
+    logger.info(f"ev: {ev}")
+
+    output_v = output_v.numpy()
+    ofv_sb = ofv_sb.numpy()
+    fig = plt.figure()
+    ax = plt.subplot(1, 2, 1)
+    utils.visualize(
+        ax,
+        coords[0, 0, 1:-1, 1:-1],
+        coords[0, 1, 1:-1, 1:-1],
+        output_v[0, 0, 1:-1, 1:-1],
+        "horizontal",
+        [0, 1],
+    )
+    utils.set_axis_label(ax, "p")
+    ax.set_title("CNN " + r"$T$")
+    ax.set_aspect("equal")
+    ax = plt.subplot(1, 2, 2)
+    utils.visualize(
+        ax,
+        coords[0, 0, 1:-1, 1:-1],
+        coords[0, 1, 1:-1, 1:-1],
+        ofv_sb[1:-1, 1:-1],
+        "horizontal",
+        [0, 1],
+    )
+    utils.set_axis_label(ax, "p")
+    ax.set_aspect("equal")
+    ax.set_title("FV " + r"$T$")
+    fig.tight_layout(pad=1)
+
+

4. 完整代码

+
heat_equation.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
from typing import Dict
+
+import hydra
+import matplotlib.pyplot as plt
+import numpy as np
+import paddle
+import utils
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    data = np.load(cfg.data_dir)
+    coords = data["coords"]
+    jinvs = data["jinvs"]
+    dxdxis = data["dxdxis"]
+    dydxis = data["dydxis"]
+    dxdetas = data["dxdetas"]
+    dydetas = data["dydetas"]
+
+    model = ppsci.arch.USCNN(**cfg.MODEL)
+
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+    iters_per_epoch = coords.shape[0]
+    sup_constraint_res = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {
+                    "coords": coords,
+                    "jinvs": jinvs,
+                    "dxdxis": dxdxis,
+                    "dydxis": dydxis,
+                    "dxdetas": dxdetas,
+                    "dydetas": dydetas,
+                },
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "iters_per_epoch": iters_per_epoch,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(
+            lambda out, label, weight: {"residual": out["residual"]}
+        ),
+        name="residual",
+    )
+    sup_constraint = {sup_constraint_res.name: sup_constraint_res}
+
+    def _transform_out(
+        _input: Dict[str, paddle.Tensor],
+        _output: Dict[str, paddle.Tensor],
+        pad_singleside: int = cfg.MODEL.pad_singleside,
+    ):
+        """Calculation residual.
+
+        Args:
+            _input (Dict[str, paddle.Tensor]): The input of the model.
+            _output (Dict[str, paddle.Tensor]): The output of the model.
+            pad_singleside (int, optional): Pad size. Defaults to cfg.MODEL.pad_singleside.
+        """
+        output_v = _output["output_v"]
+        jinv = _input["jinvs"]
+        dxdxi = _input["dxdxis"]
+        dydxi = _input["dydxis"]
+        dxdeta = _input["dxdetas"]
+        dydeta = _input["dydetas"]
+        output_v[:, 0, -pad_singleside:, pad_singleside:-pad_singleside] = 0
+        output_v[:, 0, :pad_singleside, pad_singleside:-pad_singleside] = 1
+        output_v[:, 0, pad_singleside:-pad_singleside, -pad_singleside:] = 1
+        output_v[:, 0, pad_singleside:-pad_singleside, 0:pad_singleside] = 1
+        output_v[:, 0, 0, 0] = 0.5 * (output_v[:, 0, 0, 1] + output_v[:, 0, 1, 0])
+        output_v[:, 0, 0, -1] = 0.5 * (output_v[:, 0, 0, -2] + output_v[:, 0, 1, -1])
+        dvdx = utils.dfdx(output_v, dydeta, dydxi, jinv)
+        d2vdx2 = utils.dfdx(dvdx, dydeta, dydxi, jinv)
+        dvdy = utils.dfdy(output_v, dxdxi, dxdeta, jinv)
+        d2vdy2 = utils.dfdy(dvdy, dxdxi, dxdeta, jinv)
+        continuity = d2vdy2 + d2vdx2
+        return {"residual": paddle.mean(continuity**2)}
+
+    model.register_output_transform(_transform_out)
+    solver = ppsci.solver.Solver(
+        model,
+        sup_constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.epochs,
+        iters_per_epoch=iters_per_epoch,
+    )
+    solver.train()
+    solver.plot_loss_history()
+
+
+def evaluate(cfg: DictConfig):
+    data = np.load(cfg.data_dir)
+    coords = data["coords"]
+
+    ofv_sb = paddle.to_tensor(data["OFV_sb"])
+
+    ## create model
+    pad_singleside = cfg.MODEL.pad_singleside
+    model = ppsci.arch.USCNN(**cfg.MODEL)
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,  ### the path of the model
+    )
+    output_v = solver.predict({"coords": paddle.to_tensor(coords)})
+    output_v = output_v["output_v"]
+
+    output_v[0, 0, -pad_singleside:, pad_singleside:-pad_singleside] = 0
+    output_v[0, 0, :pad_singleside, pad_singleside:-pad_singleside] = 1
+    output_v[0, 0, pad_singleside:-pad_singleside, -pad_singleside:] = 1
+    output_v[0, 0, pad_singleside:-pad_singleside, 0:pad_singleside] = 1
+    output_v[0, 0, 0, 0] = 0.5 * (output_v[0, 0, 0, 1] + output_v[0, 0, 1, 0])
+    output_v[0, 0, 0, -1] = 0.5 * (output_v[0, 0, 0, -2] + output_v[0, 0, 1, -1])
+
+    ev = paddle.sqrt(
+        paddle.mean((ofv_sb - output_v[0, 0]) ** 2) / paddle.mean(ofv_sb**2)
+    ).item()
+    logger.info(f"ev: {ev}")
+
+    output_v = output_v.numpy()
+    ofv_sb = ofv_sb.numpy()
+    fig = plt.figure()
+    ax = plt.subplot(1, 2, 1)
+    utils.visualize(
+        ax,
+        coords[0, 0, 1:-1, 1:-1],
+        coords[0, 1, 1:-1, 1:-1],
+        output_v[0, 0, 1:-1, 1:-1],
+        "horizontal",
+        [0, 1],
+    )
+    utils.set_axis_label(ax, "p")
+    ax.set_title("CNN " + r"$T$")
+    ax.set_aspect("equal")
+    ax = plt.subplot(1, 2, 2)
+    utils.visualize(
+        ax,
+        coords[0, 0, 1:-1, 1:-1],
+        coords[0, 1, 1:-1, 1:-1],
+        ofv_sb[1:-1, 1:-1],
+        "horizontal",
+        [0, 1],
+    )
+    utils.set_axis_label(ax, "p")
+    ax.set_aspect("equal")
+    ax.set_title("FV " + r"$T$")
+    fig.tight_layout(pad=1)
+    fig.savefig(f"{cfg.output_dir}/result.png", bbox_inches="tight")
+    plt.close(fig)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="heat_equation.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

Heat equation结果展示: +image

+

Heat equation with boundary 结果展示:

+

T=0 +image

+

T=3 +image

+

T=6 +iamge

+

6. 总结

+

本文通过使用调和映射构造坐标变换函数,使得物理信息网络可以在不规则非均匀网格上面进行训练,同时,因为该映射为使用传统方法进行,所以无需训练即可在网络前后嵌入。通过大量实验表明,该网络可以在各种不规则网格问题上表现比SOAT网络突出。

+

7. 参考资料

+

PhyGeoNet: Physics-informed geometry-adaptive convolutional neural networks for solving parameterized steady-state PDEs on irregular domain

+

Github PhyGeoNet

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/phylstm/index.html b/zh/examples/phylstm/index.html new file mode 100644 index 0000000000..23c670bcb6 --- /dev/null +++ b/zh/examples/phylstm/index.html @@ -0,0 +1,5363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Phy-LSTM - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

PhyLSTM

+
+
+
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat -o data_boucwen.mat
+python phylstm2.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat -o data_boucwen.mat
+python phylstm3.py
+
+
+
+
+
+
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat -o data_boucwen.mat
+python phylstm2.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/phylstm/phylstm2_pretrained.pdparams
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat -o data_boucwen.mat
+python phylstm3.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/phylstm/phylstm3_pretrained.pdparams
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
预训练模型指标
phylstm2_pretrained.pdparamsloss(sup_valid): 0.00799
phylstm3_pretrained.pdparamsloss(sup_valid): 0.03098
+

1. 背景简介

+

我们引入了一种创新的物理知识LSTM框架,用于对缺乏数据的非线性结构系统进行元建模。基本概念是将可用但尚不完整的物理知识(如物理定律、科学原理)整合到深度长短时记忆(LSTM)网络中,该网络在可行的解决方案空间内限制和促进学习。物理约束嵌入在损失函数中,以强制执行模型训练,即使在可用训练数据集非常有限的情况下,也能准确地捕捉潜在的系统非线性。特别是对于动态结构,考虑运动方程的物理定律、状态依赖性和滞后本构关系来构建物理损失。嵌入式物理可以缓解过拟合问题,减少对大型训练数据集的需求,并提高训练模型的鲁棒性,使其具有外推能力,从而进行更可靠的预测。因此,物理知识指导的深度学习范式优于传统的非物理指导的数据驱动神经网络。

+

2. 问题定义

+

结构系统的元建模旨在开发低保真度(或低阶)模型,以有效地捕捉潜在的非线性输入-输出行为。元模型可以在从高保真度模拟或实际系统感知获得的数据集上进行训练。为了更好地说明,我们考虑一个建筑类型结构并假设地震动力学由低保真度非线性运动方程(EOM)支配:

+
\[ +\mathbf{M} \ddot{\mathbf{u}}+\underbrace{\mathbf{C} \dot{\mathbf{u}}+\lambda \mathbf{K u}+(1-\lambda) \mathbf{K r}}_{\mathbf{h}}=-\mathbf{M} \Gamma a_g +\]
+

其中M是质量矩阵;C为阻尼矩阵;K为刚度矩阵。

+

控制方程可以改写成一个更一般的形式:

+
\[ +\ddot{\mathbf{u}}+\mathrm{g}=-\Gamma a_g +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 PhyLSTM 问题中,建立 LSTM 网络 Deep LSTM network,用 PaddleScience 代码表示如下

+
model = ppsci.arch.DeepPhyLSTM(
+    cfg.MODEL.input_size,
+    eta.shape[2],
+    cfg.MODEL.hidden_size,
+    cfg.MODEL.model_type,
+)
+
+

DeepPhyLSTM 参数 input_size 是输入大小,output_size 是输出大小,hidden_size 是隐藏层大小,model_type是模型类型。

+

3.2 数据构建

+

运行本问题代码前请按照下方命令下载 data_boucwen.mat

+
wget -nc -P ./ https://paddle-org.bj.bcebos.com/paddlescience/datasets/PhyLSTM/data_boucwen.mat
+
+

本案例涉及读取数据构建,如下所示

+
mat = scipy.io.loadmat(cfg.DATA_FILE_PATH)
+ag_data = mat["input_tf"]  # ag, ad, av
+u_data = mat["target_X_tf"]
+ut_data = mat["target_Xd_tf"]
+utt_data = mat["target_Xdd_tf"]
+ag_data = ag_data.reshape([ag_data.shape[0], ag_data.shape[1], 1])
+u_data = u_data.reshape([u_data.shape[0], u_data.shape[1], 1])
+ut_data = ut_data.reshape([ut_data.shape[0], ut_data.shape[1], 1])
+utt_data = utt_data.reshape([utt_data.shape[0], utt_data.shape[1], 1])
+
+t = mat["time"]
+dt = t[0, 1] - t[0, 0]
+
+ag_all = ag_data
+u_all = u_data
+u_t_all = ut_data
+u_tt_all = utt_data
+
+# finite difference
+N = u_data.shape[1]
+phi1 = np.concatenate(
+    [
+        np.array([-3 / 2, 2, -1 / 2]),
+        np.zeros([N - 3]),
+    ]
+)
+temp1 = np.concatenate([-1 / 2 * np.identity(N - 2), np.zeros([N - 2, 2])], axis=1)
+temp2 = np.concatenate([np.zeros([N - 2, 2]), 1 / 2 * np.identity(N - 2)], axis=1)
+phi2 = temp1 + temp2
+phi3 = np.concatenate(
+    [
+        np.zeros([N - 3]),
+        np.array([1 / 2, -2, 3 / 2]),
+    ]
+)
+phi_t0 = (
+    1
+    / dt
+    * np.concatenate(
+        [
+            np.reshape(phi1, [1, phi1.shape[0]]),
+            phi2,
+            np.reshape(phi3, [1, phi3.shape[0]]),
+        ],
+        axis=0,
+    )
+)
+phi_t0 = np.reshape(phi_t0, [1, N, N])
+
+ag_star = ag_all[0:10]
+eta_star = u_all[0:10]
+eta_t_star = u_t_all[0:10]
+eta_tt_star = u_tt_all[0:10]
+ag_c_star = ag_all[0:50]
+lift_star = -ag_c_star
+
+eta = eta_star
+ag = ag_star
+lift = lift_star
+eta_t = eta_t_star
+eta_tt = eta_tt_star
+ag_c = ag_c_star
+g = -eta_tt - ag
+phi_t = np.repeat(phi_t0, ag_c_star.shape[0], axis=0)
+
+

3.3 约束构建

+

设置训练数据集和损失计算函数,返回字段,代码如下所示:

+
sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_train,
+            "label": label_dict_train,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.train_loss_func2),
+    {
+        "eta_pred": lambda out: out["eta_pred"],
+        "eta_dot_pred": lambda out: out["eta_dot_pred"],
+        "g_pred": lambda out: out["g_pred"],
+        "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+        "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+        "lift_pred_c": lambda out: out["lift_pred_c"],
+    },
+    name="sup_train",
+)
+constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+

3.4 评估器构建

+

设置评估数据集和损失计算函数,返回字段,代码如下所示:

+
sup_validator_pde = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": input_dict_val,
+            "label": label_dict_val,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": 1,
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(functions.train_loss_func2),
+    {
+        "eta_pred": lambda out: out["eta_pred"],
+        "eta_dot_pred": lambda out: out["eta_dot_pred"],
+        "g_pred": lambda out: out["g_pred"],
+        "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+        "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+        "lift_pred_c": lambda out: out["lift_pred_c"],
+    },
+    metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+    name="sup_valid",
+)
+validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数,此处我们按实验经验,使用 100 轮训练轮数。

+
epochs: 100
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择 Adam 优化器并设定 learning_rate 为 1e-3。

+
optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+
+

3.7 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
solver = ppsci.solver.Solver(
+    model,
+    constraint_pde,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    log_freq=cfg.log_freq,
+    seed=cfg.seed,
+    validator=validator_pde,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+
+

最后启动训练、评估即可:

+
# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

4. 完整代码

+
+
+
+
phylstm2.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/zhry10/PhyLSTM.git
+"""
+
+from os import path as osp
+
+import functions
+import hydra
+import numpy as np
+import scipy.io
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    mat = scipy.io.loadmat(cfg.DATA_FILE_PATH)
+    ag_data = mat["input_tf"]  # ag, ad, av
+    u_data = mat["target_X_tf"]
+    ut_data = mat["target_Xd_tf"]
+    utt_data = mat["target_Xdd_tf"]
+    ag_data = ag_data.reshape([ag_data.shape[0], ag_data.shape[1], 1])
+    u_data = u_data.reshape([u_data.shape[0], u_data.shape[1], 1])
+    ut_data = ut_data.reshape([ut_data.shape[0], ut_data.shape[1], 1])
+    utt_data = utt_data.reshape([utt_data.shape[0], utt_data.shape[1], 1])
+
+    t = mat["time"]
+    dt = t[0, 1] - t[0, 0]
+
+    ag_all = ag_data
+    u_all = u_data
+    u_t_all = ut_data
+    u_tt_all = utt_data
+
+    # finite difference
+    N = u_data.shape[1]
+    phi1 = np.concatenate(
+        [
+            np.array([-3 / 2, 2, -1 / 2]),
+            np.zeros([N - 3]),
+        ]
+    )
+    temp1 = np.concatenate([-1 / 2 * np.identity(N - 2), np.zeros([N - 2, 2])], axis=1)
+    temp2 = np.concatenate([np.zeros([N - 2, 2]), 1 / 2 * np.identity(N - 2)], axis=1)
+    phi2 = temp1 + temp2
+    phi3 = np.concatenate(
+        [
+            np.zeros([N - 3]),
+            np.array([1 / 2, -2, 3 / 2]),
+        ]
+    )
+    phi_t0 = (
+        1
+        / dt
+        * np.concatenate(
+            [
+                np.reshape(phi1, [1, phi1.shape[0]]),
+                phi2,
+                np.reshape(phi3, [1, phi3.shape[0]]),
+            ],
+            axis=0,
+        )
+    )
+    phi_t0 = np.reshape(phi_t0, [1, N, N])
+
+    ag_star = ag_all[0:10]
+    eta_star = u_all[0:10]
+    eta_t_star = u_t_all[0:10]
+    eta_tt_star = u_tt_all[0:10]
+    ag_c_star = ag_all[0:50]
+    lift_star = -ag_c_star
+
+    eta = eta_star
+    ag = ag_star
+    lift = lift_star
+    eta_t = eta_t_star
+    eta_tt = eta_tt_star
+    ag_c = ag_c_star
+    g = -eta_tt - ag
+    phi_t = np.repeat(phi_t0, ag_c_star.shape[0], axis=0)
+
+    model = ppsci.arch.DeepPhyLSTM(
+        cfg.MODEL.input_size,
+        eta.shape[2],
+        cfg.MODEL.hidden_size,
+        cfg.MODEL.model_type,
+    )
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(functions.transform_out)
+
+    dataset_obj = functions.Dataset(eta, eta_t, g, ag, ag_c, lift, phi_t)
+    (
+        input_dict_train,
+        label_dict_train,
+        input_dict_val,
+        label_dict_val,
+    ) = dataset_obj.get(cfg.TRAIN.epochs)
+
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_train,
+                "label": label_dict_train,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": True,
+                "shuffle": True,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func2),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+        },
+        name="sup_train",
+    )
+    constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func2),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+        },
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+    solver = ppsci.solver.Solver(
+        model,
+        constraint_pde,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator_pde,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    mat = scipy.io.loadmat(cfg.DATA_FILE_PATH)
+    ag_data = mat["input_tf"]  # ag, ad, av
+    u_data = mat["target_X_tf"]
+    ut_data = mat["target_Xd_tf"]
+    utt_data = mat["target_Xdd_tf"]
+    ag_data = ag_data.reshape([ag_data.shape[0], ag_data.shape[1], 1])
+    u_data = u_data.reshape([u_data.shape[0], u_data.shape[1], 1])
+    ut_data = ut_data.reshape([ut_data.shape[0], ut_data.shape[1], 1])
+    utt_data = utt_data.reshape([utt_data.shape[0], utt_data.shape[1], 1])
+
+    t = mat["time"]
+    dt = t[0, 1] - t[0, 0]
+
+    ag_all = ag_data
+    u_all = u_data
+    u_t_all = ut_data
+    u_tt_all = utt_data
+
+    # finite difference
+    N = u_data.shape[1]
+    phi1 = np.concatenate(
+        [
+            np.array([-3 / 2, 2, -1 / 2]),
+            np.zeros([N - 3]),
+        ]
+    )
+    temp1 = np.concatenate([-1 / 2 * np.identity(N - 2), np.zeros([N - 2, 2])], axis=1)
+    temp2 = np.concatenate([np.zeros([N - 2, 2]), 1 / 2 * np.identity(N - 2)], axis=1)
+    phi2 = temp1 + temp2
+    phi3 = np.concatenate(
+        [
+            np.zeros([N - 3]),
+            np.array([1 / 2, -2, 3 / 2]),
+        ]
+    )
+    phi_t0 = (
+        1
+        / dt
+        * np.concatenate(
+            [
+                np.reshape(phi1, [1, phi1.shape[0]]),
+                phi2,
+                np.reshape(phi3, [1, phi3.shape[0]]),
+            ],
+            axis=0,
+        )
+    )
+    phi_t0 = np.reshape(phi_t0, [1, N, N])
+
+    ag_star = ag_all[0:10]
+    eta_star = u_all[0:10]
+    eta_t_star = u_t_all[0:10]
+    eta_tt_star = u_tt_all[0:10]
+    ag_c_star = ag_all[0:50]
+    lift_star = -ag_c_star
+
+    eta = eta_star
+    ag = ag_star
+    lift = lift_star
+    eta_t = eta_t_star
+    eta_tt = eta_tt_star
+    ag_c = ag_c_star
+    g = -eta_tt - ag
+    phi_t = np.repeat(phi_t0, ag_c_star.shape[0], axis=0)
+
+    model = ppsci.arch.DeepPhyLSTM(
+        cfg.MODEL.input_size,
+        eta.shape[2],
+        cfg.MODEL.hidden_size,
+        cfg.MODEL.model_type,
+    )
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(functions.transform_out)
+
+    dataset_obj = functions.Dataset(eta, eta_t, g, ag, ag_c, lift, phi_t)
+    (
+        _,
+        _,
+        input_dict_val,
+        label_dict_val,
+    ) = dataset_obj.get(1)
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func2),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+        },
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        validator=validator_pde,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate
+    solver.eval()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="phylstm2.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
+
+
phylstm3.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Reference: https://github.com/zhry10/PhyLSTM.git
+"""
+
+from os import path as osp
+
+import functions
+import hydra
+import numpy as np
+import scipy.io
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    mat = scipy.io.loadmat(cfg.DATA_FILE_PATH)
+    t = mat["time"]
+    dt = 0.02
+    n1 = int(dt / 0.005)
+    t = t[::n1]
+
+    ag_data = mat["input_tf"][:, ::n1]  # ag, ad, av
+    u_data = mat["target_X_tf"][:, ::n1]
+    ut_data = mat["target_Xd_tf"][:, ::n1]
+    utt_data = mat["target_Xdd_tf"][:, ::n1]
+    ag_data = ag_data.reshape([ag_data.shape[0], ag_data.shape[1], 1])
+    u_data = u_data.reshape([u_data.shape[0], u_data.shape[1], 1])
+    ut_data = ut_data.reshape([ut_data.shape[0], ut_data.shape[1], 1])
+    utt_data = utt_data.reshape([utt_data.shape[0], utt_data.shape[1], 1])
+
+    ag_pred = mat["input_pred_tf"][:, ::n1]  # ag, ad, av
+    u_pred = mat["target_pred_X_tf"][:, ::n1]
+    ut_pred = mat["target_pred_Xd_tf"][:, ::n1]
+    utt_pred = mat["target_pred_Xdd_tf"][:, ::n1]
+    ag_pred = ag_pred.reshape([ag_pred.shape[0], ag_pred.shape[1], 1])
+    u_pred = u_pred.reshape([u_pred.shape[0], u_pred.shape[1], 1])
+    ut_pred = ut_pred.reshape([ut_pred.shape[0], ut_pred.shape[1], 1])
+    utt_pred = utt_pred.reshape([utt_pred.shape[0], utt_pred.shape[1], 1])
+
+    N = u_data.shape[1]
+    phi1 = np.concatenate(
+        [
+            np.array([-3 / 2, 2, -1 / 2]),
+            np.zeros([N - 3]),
+        ]
+    )
+    temp1 = np.concatenate([-1 / 2 * np.identity(N - 2), np.zeros([N - 2, 2])], axis=1)
+    temp2 = np.concatenate([np.zeros([N - 2, 2]), 1 / 2 * np.identity(N - 2)], axis=1)
+    phi2 = temp1 + temp2
+    phi3 = np.concatenate(
+        [
+            np.zeros([N - 3]),
+            np.array([1 / 2, -2, 3 / 2]),
+        ]
+    )
+    phi_t0 = (
+        1
+        / dt
+        * np.concatenate(
+            [
+                np.reshape(phi1, [1, phi1.shape[0]]),
+                phi2,
+                np.reshape(phi3, [1, phi3.shape[0]]),
+            ],
+            axis=0,
+        )
+    )
+    phi_t0 = np.reshape(phi_t0, [1, N, N])
+
+    ag_star = ag_data
+    eta_star = u_data
+    eta_t_star = ut_data
+    eta_tt_star = utt_data
+    ag_c_star = np.concatenate([ag_data, ag_pred[0:53]])
+    lift_star = -ag_c_star
+
+    eta = eta_star
+    ag = ag_star
+    lift = lift_star
+    eta_t = eta_t_star
+    eta_tt = eta_tt_star
+    g = -eta_tt - ag
+    ag_c = ag_c_star
+
+    phi_t = np.repeat(phi_t0, ag_c_star.shape[0], axis=0)
+
+    model = ppsci.arch.DeepPhyLSTM(
+        cfg.MODEL.input_size,
+        eta.shape[2],
+        cfg.MODEL.hidden_size,
+        cfg.MODEL.model_type,
+    )
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(functions.transform_out)
+
+    dataset_obj = functions.Dataset(eta, eta_t, g, ag, ag_c, lift, phi_t)
+    (
+        input_dict_train,
+        label_dict_train,
+        input_dict_val,
+        label_dict_val,
+    ) = dataset_obj.get(cfg.TRAIN.epochs)
+
+    sup_constraint_pde = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_train,
+                "label": label_dict_train,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func3),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+            "g_t_pred_c": lambda out: out["g_t_pred_c"],
+            "g_dot_pred_c": lambda out: out["g_dot_pred_c"],
+        },
+        name="sup_train",
+    )
+    constraint_pde = {sup_constraint_pde.name: sup_constraint_pde}
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func3),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+            "g_t_pred_c": lambda out: out["g_t_pred_c"],
+            "g_dot_pred_c": lambda out: out["g_dot_pred_c"],
+        },
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)
+    solver = ppsci.solver.Solver(
+        model,
+        constraint_pde,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        validator=validator_pde,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    mat = scipy.io.loadmat(cfg.DATA_FILE_PATH)
+    t = mat["time"]
+    dt = 0.02
+    n1 = int(dt / 0.005)
+    t = t[::n1]
+
+    ag_data = mat["input_tf"][:, ::n1]  # ag, ad, av
+    u_data = mat["target_X_tf"][:, ::n1]
+    ut_data = mat["target_Xd_tf"][:, ::n1]
+    utt_data = mat["target_Xdd_tf"][:, ::n1]
+    ag_data = ag_data.reshape([ag_data.shape[0], ag_data.shape[1], 1])
+    u_data = u_data.reshape([u_data.shape[0], u_data.shape[1], 1])
+    ut_data = ut_data.reshape([ut_data.shape[0], ut_data.shape[1], 1])
+    utt_data = utt_data.reshape([utt_data.shape[0], utt_data.shape[1], 1])
+
+    ag_pred = mat["input_pred_tf"][:, ::n1]  # ag, ad, av
+    u_pred = mat["target_pred_X_tf"][:, ::n1]
+    ut_pred = mat["target_pred_Xd_tf"][:, ::n1]
+    utt_pred = mat["target_pred_Xdd_tf"][:, ::n1]
+    ag_pred = ag_pred.reshape([ag_pred.shape[0], ag_pred.shape[1], 1])
+    u_pred = u_pred.reshape([u_pred.shape[0], u_pred.shape[1], 1])
+    ut_pred = ut_pred.reshape([ut_pred.shape[0], ut_pred.shape[1], 1])
+    utt_pred = utt_pred.reshape([utt_pred.shape[0], utt_pred.shape[1], 1])
+
+    N = u_data.shape[1]
+    phi1 = np.concatenate(
+        [
+            np.array([-3 / 2, 2, -1 / 2]),
+            np.zeros([N - 3]),
+        ]
+    )
+    temp1 = np.concatenate([-1 / 2 * np.identity(N - 2), np.zeros([N - 2, 2])], axis=1)
+    temp2 = np.concatenate([np.zeros([N - 2, 2]), 1 / 2 * np.identity(N - 2)], axis=1)
+    phi2 = temp1 + temp2
+    phi3 = np.concatenate(
+        [
+            np.zeros([N - 3]),
+            np.array([1 / 2, -2, 3 / 2]),
+        ]
+    )
+    phi_t0 = (
+        1
+        / dt
+        * np.concatenate(
+            [
+                np.reshape(phi1, [1, phi1.shape[0]]),
+                phi2,
+                np.reshape(phi3, [1, phi3.shape[0]]),
+            ],
+            axis=0,
+        )
+    )
+    phi_t0 = np.reshape(phi_t0, [1, N, N])
+
+    ag_star = ag_data
+    eta_star = u_data
+    eta_t_star = ut_data
+    eta_tt_star = utt_data
+    ag_c_star = np.concatenate([ag_data, ag_pred[0:53]])
+    lift_star = -ag_c_star
+
+    eta = eta_star
+    ag = ag_star
+    lift = lift_star
+    eta_t = eta_t_star
+    eta_tt = eta_tt_star
+    g = -eta_tt - ag
+    ag_c = ag_c_star
+
+    phi_t = np.repeat(phi_t0, ag_c_star.shape[0], axis=0)
+
+    model = ppsci.arch.DeepPhyLSTM(
+        cfg.MODEL.input_size,
+        eta.shape[2],
+        cfg.MODEL.hidden_size,
+        cfg.MODEL.model_type,
+    )
+    model.register_input_transform(functions.transform_in)
+    model.register_output_transform(functions.transform_out)
+
+    dataset_obj = functions.Dataset(eta, eta_t, g, ag, ag_c, lift, phi_t)
+    (
+        _,
+        _,
+        input_dict_val,
+        label_dict_val,
+    ) = dataset_obj.get(1)
+
+    sup_validator_pde = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": input_dict_val,
+                "label": label_dict_val,
+            },
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+            "batch_size": 1,
+            "num_workers": 0,
+        },
+        ppsci.loss.FunctionalLoss(functions.train_loss_func3),
+        {
+            "eta_pred": lambda out: out["eta_pred"],
+            "eta_dot_pred": lambda out: out["eta_dot_pred"],
+            "g_pred": lambda out: out["g_pred"],
+            "eta_t_pred_c": lambda out: out["eta_t_pred_c"],
+            "eta_dot_pred_c": lambda out: out["eta_dot_pred_c"],
+            "lift_pred_c": lambda out: out["lift_pred_c"],
+            "g_t_pred_c": lambda out: out["g_t_pred_c"],
+            "g_dot_pred_c": lambda out: out["g_dot_pred_c"],
+        },
+        metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)},
+        name="sup_valid",
+    )
+    validator_pde = {sup_validator_pde.name: sup_validator_pde}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        validator=validator_pde,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+
+    # evaluate
+    solver.eval()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="phylstm3.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
+
+
+

5. 结果展示

+

PhyLSTM2 案例针对 epoch=100 和 learning_rate=1e-3 的参数配置进行了实验,结果返回Loss为 0.00799。

+

PhyLSTM3 案例针对 epoch=200 和 learning_rate=1e-3 的参数配置进行了实验,结果返回Loss为 0.03098。

+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/pirbn/index.html b/zh/examples/pirbn/index.html new file mode 100644 index 0000000000..e76a73ea79 --- /dev/null +++ b/zh/examples/pirbn/index.html @@ -0,0 +1,3907 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PIRBN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

PIRBN

+
+
+
+
cd PaddleScience/jointContribution/PIRBN
+python main.py
+
+
+
+
+

1. 背景简介

+

我们最近发现经过训练,物理信息神经网络(PINN)往往会成为局部近似函数。这一观察结果促使我们开发了一种新型的物理-信息径向基网络(PIRBN),该网络在整个训练过程中都能够维持局部近似性质。与深度神经网络不同,PIRBN 仅包含一个隐藏层和一个径向基“激活”函数。在适当的条件下,我们证明了使用梯度下降方法训练 PIRBN 可以收敛到高斯过程。此外,我们还通过神经邻近核(NTK)理论研究了 PIRBN 的训练动态。此外,我们还对 PIRBN 的初始化策略进行了全面调查。基于数值示例,我们发现 PIRBN 在解决具有高频特征和病态计算域的非线性偏微分方程方面比PINN更有效。此外,现有的 PINN 数值技术,如自适应学习、分解和不同类型的损失函数,也适用于 PIRBN。

+
+

介绍 +

+
网络的结构
+
+

图片左侧为常见神经网络结构的输入层,隐藏层,输出层,隐藏层包含激活层,a 中为单层隐藏层,b 中为多层隐藏层,图片右侧为 PIRBN 网络的激活函数,计算网络的损失 Loss 并反向传递。图片说明当使用 PIRBN 时,每个 RBF 神经元仅在输入接近神经元中心时被激活。直观地说,PIRBN 具有局部逼近特性。通过梯度下降算法训练一个 PIRBN 也可以通过 NTK 理论进行分析。

+
+

gaussian +

+
不同阶数的高斯激活函数
+
+

(a) 0, 1, 2 阶高斯激活函数 +(b) 设置不同 b 值 +(c) 设置不同 c 值

+

当使用高斯函数作为激活函数时,输入与输出之间的映射关系可以数学上表示为高斯函数的某种形式。RBF 网络是一种常用于模式识别、数据插值和函数逼近的神经网络,其关键特征是使用径向基函数作为激活函数,使得网络具有更好的全局逼近能力和灵活性。

+

2. 问题定义

+

在 NTK 和基于 NTK 的适应性训练方法的帮助下,PINN 在处理具有高频特征的问题时的性能可以得到显著提升。例如,考虑一个偏微分方程及其边界条件:

+
\[ +\begin{aligned} +& \frac{\mathrm{d}^2}{\mathrm{~d} x^2} u(x)-4 \mu^2 \pi^2 \sin (2 \mu \pi x)=0, \text { for } x \in[0,1] \\ +& u(0)=u(1)=0 +\end{aligned} +\]
+

其中\(\mu\)是一个控制PDE解的频率特征的常数。

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddlePaddle 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddlePaddle,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 PIRBN 问题中,建立网络,用 PaddlePaddle 代码表示如下

+
40
+41
+42
# Set up PIRBN
+rbn = rbn_net.RBN_Net(n_in, n_out, n_neu, b, c, activation_function)
+rbn_loss = pirbn.PIRBN(rbn, activation_function)
+
+

3.2 数据构建

+

本案例涉及读取数据构建,如下所示

+
18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
# Define the number of sample points
+ns = 50
+
+# Define the sample points' interval
+dx = 1.0 / (ns - 1)
+
+# Initialise sample points' coordinates
+x_eq = np.linspace(0.0, 1.0, ns)[:, None]
+
+for i in range(0, ns):
+    x_eq[i, 0] = i * dx + right_by
+x_bc = np.array([[right_by + 0.0], [right_by + 1.0]])
+x = [x_eq, x_bc]
+y = -4 * mu**2 * np.pi**2 * np.sin(2 * mu * np.pi * x_eq)
+
+# Set up radial basis network
+n_in = 1
+n_out = 1
+n_neu = 61
+b = 10.0
+c = [right_by - 0.1, right_by + 1.1]
+
+

3.3 训练和评估构建

+

训练和评估构建,设置损失计算函数,返回字段,代码如下所示:

+
52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
def evaluate(self):
+    # compute loss
+    loss, loss_g, loss_b = self.Loss(self.x_train, self.y_train, self.a_g, self.a_b)
+    loss_g_numpy = float(loss_g)
+    loss_b_numpy = float(loss_b)
+    # eq loss
+    self.loss_g.append(loss_g_numpy)
+    # boundary loss
+    self.loss_b.append(loss_b_numpy)
+    if self.iter % 100 == 0:
+        if self.adaptive_weights:
+            self.a_g, self.a_b, _ = self.pirbn.cal_ntk(self.x_train)
+            print(
+                "Iter : ",
+                self.iter,
+                "\tloss : ",
+                float(loss),
+                "\tboundary loss : ",
+                float(loss_b),
+                "\teq loss : ",
+                float(loss_g),
+            )
+            print("\ta_g =", float(self.a_g), "\ta_b =", float(self.a_b))
+        else:
+            print(
+                "Iter : ",
+                self.iter,
+                "\tloss : ",
+                float(loss),
+                "\tboundary loss : ",
+                float(loss_b),
+                "\teq loss : ",
+                float(loss_g),
+            )
+    self.his_a_g.append(self.a_g)
+    self.his_a_b.append(self.a_b)
+
+    self.iter = self.iter + 1
+    return loss
+
+

3.4 超参数设定

+

接下来我们需要指定训练轮数,此处我们按实验经验,使用 20001 轮训练轮数。

+
maxiter = 20001
+
+

3.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择 Adam 优化器并设定 learning_rate 为 1e-3。

+
33
+34
+35
self.optimizer = paddle.optimizer.Adam(
+    learning_rate=0.001, parameters=self.pirbn.parameters()
+)
+
+

3.6 模型训练与评估

+

模型训练与评估

+
92
+93
+94
+95
+96
+97
+98
+99
def fit(self, output_Kgg):
+    for i in range(0, self.maxiter):
+        loss = self.evaluate()
+        loss.backward()
+        if i in output_Kgg:
+            self.ntk_list[f"{i}"] = self.pirbn.cal_K(self.x_train)
+        self.optimizer.step()
+        self.optimizer.clear_grad()
+
+

4. 完整代码

+
main.py
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
import analytical_solution
+import numpy as np
+import pirbn
+import rbn_net
+import train
+
+import ppsci
+
+# set random seed for reproducibility
+SEED = 2023
+ppsci.utils.misc.set_random_seed(SEED)
+
+# mu, Fig.1, Page5
+# right_by, Formula (15) Page5
+def sine_function_main(
+    mu, adaptive_weights=True, right_by=0, activation_function="gaussian"
+):
+    # Define the number of sample points
+    ns = 50
+
+    # Define the sample points' interval
+    dx = 1.0 / (ns - 1)
+
+    # Initialise sample points' coordinates
+    x_eq = np.linspace(0.0, 1.0, ns)[:, None]
+
+    for i in range(0, ns):
+        x_eq[i, 0] = i * dx + right_by
+    x_bc = np.array([[right_by + 0.0], [right_by + 1.0]])
+    x = [x_eq, x_bc]
+    y = -4 * mu**2 * np.pi**2 * np.sin(2 * mu * np.pi * x_eq)
+
+    # Set up radial basis network
+    n_in = 1
+    n_out = 1
+    n_neu = 61
+    b = 10.0
+    c = [right_by - 0.1, right_by + 1.1]
+
+    # Set up PIRBN
+    rbn = rbn_net.RBN_Net(n_in, n_out, n_neu, b, c, activation_function)
+    rbn_loss = pirbn.PIRBN(rbn, activation_function)
+    maxiter = 20001
+    output_Kgg = [0, int(0.1 * maxiter), maxiter - 1]
+    train_obj = train.Trainer(
+        rbn_loss,
+        x,
+        y,
+        learning_rate=0.001,
+        maxiter=maxiter,
+        adaptive_weights=adaptive_weights,
+    )
+    train_obj.fit(output_Kgg)
+
+    # Visualise results
+    analytical_solution.output_fig(
+        train_obj, mu, b, right_by, activation_function, output_Kgg
+    )
+
+
+# Fig.1
+sine_function_main(mu=4, right_by=0, activation_function="tanh")
+# Fig.2
+sine_function_main(mu=8, right_by=0, activation_function="tanh")
+# Fig.3
+sine_function_main(mu=4, right_by=100, activation_function="tanh")
+# Fig.6
+sine_function_main(mu=8, right_by=100, activation_function="gaussian")
+
+

5. 结果展示

+

PINN 案例针对 epoch=20001 和 learning_rate=1e-3 的参数配置进行了实验,结果返回Loss为 0.13567。

+

PIRBN 案例针对 epoch=20001 和 learning_rate=1e-3 的参数配置进行了实验,结果返回Loss为 0.59471。

+
+

PINN +

+
PINN 结果图
+
+

图为使用双曲正切函数(tanh)作为激活函数(activation function),并且使用 LuCun 初始化方法来初始化神经网络中的所有参数。

+
    +
  • 图中子图 1 为预测值和真实值的曲线比较
  • +
  • 图中子图 2 为误差值
  • +
  • 图中子图 3 为损失值
  • +
  • 图中子图 4 为训练 1 次的 Kg 图
  • +
  • 图中子图 5 为训练 2000 次的 Kg 图
  • +
  • 图中子图 6 为训练 20000 次的 Kg 图
  • +
+

可以看到预测值和真实值可以匹配,误差值逐渐升高然后逐渐减少,Loss 历史降低后波动,Kg 图随训练次数增加而逐渐收敛。

+
+

PIRBN +

+
PIRBN 结果图
+
+

图为使用高斯函数(gaussian function)作为激活函数(activation function)生成的数据,并且使用 LuCun 初始化方法来初始化神经网络中的所有参数。

+
    +
  • 图中子图 1 为预测值和真实值的曲线比较
  • +
  • 图中子图 2 为误差值
  • +
  • 图中子图 3 为损失值
  • +
  • 图中子图 4 为训练 1 次的 Kg 图
  • +
  • 图中子图 5 为训练 2000 次的 Kg 图
  • +
  • 图中子图 6 为训练 20000 次的 Kg 图
  • +
+

可以看到预测值和真实值可以匹配,误差值逐渐升高然后逐渐减少再升高,Loss 历史降低后波动,Kg 图随训练次数增加而逐渐收敛。

+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/rossler/index.html b/zh/examples/rossler/index.html new file mode 100644 index 0000000000..44f4a031e4 --- /dev/null +++ b/zh/examples/rossler/index.html @@ -0,0 +1,5542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rossler_transform_physx - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Rossler System

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 --create-dirs -o ./datasets/rossler_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 --create-dirs -o ./datasets/rossler_valid.hdf5
+python train_enn.py
+python train_transformer.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 --create-dirs -o ./datasets/rossler_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 --create-dirs -o ./datasets/rossler_valid.hdf5
+python train_enn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/rossler/rossler_pretrained.pdparams
+python train_transformer.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/rossler/rossler_transformer_pretrained.pdparams EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/rossler/rossler_pretrained.pdparams
+
+
+
+
python train_transformer.py mode=export EMBEDDING_MODEL_PATH=https://paddle-org.bj.bcebos.com/paddlescience/models/rossler/rossler_pretrained.pdparams
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 -P ./datasets/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_training.hdf5 --create-dirs -o ./datasets/rossler_training.hdf5
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/transformer_physx/rossler_valid.hdf5 --create-dirs -o ./datasets/rossler_valid.hdf5
+python train_transformer.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
模型MSE
rossler_transformer_pretrained.pdparams0.022
+

1. 背景简介

+

Rossler System,最早由德国科学家 Rossler 提出,也是常见的混沌系统。该系统在混沌理论的研究中具有重要地位,为混沌现象提供了一种数学描述和理解方法。同时由于该系统对数值扰动极为敏感,因此也是是评估机器学习(深度学习)模型准确性的良好基准。

+

2. 问题定义

+

Rossler 系统的状态方程:

+
\[ +\begin{cases} + \dfrac{\partial x}{\partial t} = -\omega y - z, & \\ + \dfrac{\partial y}{\partial t} = \omega x + \alpha y, & \\ + \dfrac{\partial z}{\partial t} = \beta + z(x - \gamma) +\end{cases} +\]
+

当参数取以下值时,系统表现出经典的混沌特性:

+
\[\omega = 1.0, \alpha = 0.165, \beta = 0.2, \gamma = 10\]
+

在这个案例中,要求给定初始时刻点的坐标,预测未来一段时间内点的运动轨迹。

+

3. 问题求解

+

接下来开始讲解如何基于 PaddleScience 代码,用深度学习的方法求解该问题。本案例基于论文 Transformers for Modeling Physical Systems 方法进行求解,关于该方法的理论部分请参考此文档原论文。接下来首先会对使用的数据集进行介绍,然后对该方法两个训练步骤(Embedding 模型训练、Transformer 模型训练)的监督约束构建、模型构建等进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集采用了 Transformer-Physx 中提供的数据。该数据集使用龙格-库塔(Runge-Kutta)传统数值求解方法得到,数据集的划分如下:

+ + + + + + + + + + + + + + + + + + + + + + + +
数据集时间序列的数量时间步的数量下载地址
训练集2561025rossler_training.hdf5
验证集321025rossler_valid.hdf5
+

数据集官网为:https://zenodo.org/record/5148524#.ZDe77-xByrc

+

3.2 Embedding 模型

+

首先展示代码中定义的各个参数变量,每个参数的具体含义会在下面使用到时进行解释。

+
examples/rossler/conf/enn.yaml
22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
# general settings
+mode: train # running mode: train/eval
+seed: 6
+output_dir: ${hydra:run.dir}
+TRAIN_BLOCK_SIZE: 16
+VALID_BLOCK_SIZE: 32
+TRAIN_FILE_PATH: ./datasets/rossler_training.hdf5
+VALID_FILE_PATH: ./datasets/rossler_valid.hdf5
+
+# model settings
+MODEL:
+  input_keys: ["states"]
+  output_keys: ["pred_states", "recover_states"]
+
+

3.2.1 约束构建

+

本案例基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/rossler/train_enn.py
55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
train_dataloader_cfg = {
+    "dataset": {
+        "name": "RosslerDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 16,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

其中,"dataset" 字段定义了使用的 Dataset 类名为 RosslerDataset,另外还指定了该类初始化时参数的取值:

+
    +
  1. file_path:代表训练数据集的文件路径,指定为变量 train_file_path 的值;
  2. +
  3. input_keys:代表模型输入数据的变量名称,此处填入变量 input_keys
  4. +
  5. label_keys:代表真实标签的变量名称,此处填入变量 output_keys
  6. +
  7. block_size:代表使用多长的时间步进行训练,指定为变量 train_block_size 的值;
  8. +
  9. stride:代表连续的两个训练样本之间的时间步间隔,指定为16;
  10. +
  11. weight_dict:代表模型输出各个变量与真实标签损失函数的权重,此处使用 output_keysweights 生成。
  12. +
+

"sampler" 字段定义了使用的 Sampler 类名为 BatchSampler,另外还指定了该类初始化时参数 drop_lastshuffle 均为 True

+

train_dataloader_cfg 还定义了 batch_sizenum_workers 的值。

+

定义监督约束的代码如下:

+
examples/rossler/train_enn.py
76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELossWithL2Decay(
+        regularization_dict={regularization_key: 1e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)}
+    ),
+    {
+        key: lambda out, k=key: out[k]
+        for key in cfg.MODEL.output_keys + (regularization_key,)
+    },
+    name="Sup",
+)
+
+

SupervisedConstraint 的第一个参数是数据的加载方式,这里使用上文中定义的 train_dataloader_cfg

+

第二个参数是损失函数的定义,这里使用带有 L2Decay 的 MSELoss,类名为 MSELossWithL2Decayregularization_dict 设置了正则化的变量名称和对应的权重;

+

第三个参数表示在训练时如何计算需要被约束的中间变量,此处我们约束的变量就是网络的输出;

+

第四个参数是约束条件的名字,方便后续对其索引。此处命名为 "Sup"。

+

3.2.2 模型构建

+

在该案例中,Embedding 模型的输入输出都是物理空间中点的位置坐标 \((x, y, z)\) ,使用了全连接层实现 Embedding 模型,如下图所示。

+
+

rossler_embedding +

+
Embedding 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/rossler/train_enn.py
92
+93
+94
+95
+96
+97
+98
+99
# manually init model
+data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+model = ppsci.arch.RosslerEmbedding(
+    cfg.MODEL.input_keys,
+    cfg.MODEL.output_keys + (regularization_key,),
+    data_mean,
+    data_std,
+)
+
+

其中,RosslerEmbedding 的前两个参数在前文中已有描述,这里不再赘述,网络模型的第三、四个参数是训练数据集的均值和方差,用于归一化输入数据。计算均值、方差的的代码表示如下:

+
examples/rossler/train_enn.py
32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
def get_mean_std(data: np.ndarray):
+    mean = np.asarray(
+        [np.mean(data[:, :, 0]), np.mean(data[:, :, 1]), np.min(data[:, :, 2])]
+    ).reshape(1, 3)
+    std = np.asarray(
+        [
+            np.std(data[:, :, 0]),
+            np.std(data[:, :, 1]),
+            np.max(data[:, :, 2]) - np.min(data[:, :, 2]),
+        ]
+    ).reshape(1, 3)
+    return mean, std
+
+

3.2.3 学习率与优化器构建

+

本案例中使用的学习率方法为 ExponentialDecay ,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下

+
examples/rossler/train_enn.py
# init optimizer and lr scheduler
+clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+    iters_per_epoch=ITERS_PER_EPOCH,
+    decay_steps=ITERS_PER_EPOCH,
+    **cfg.TRAIN.lr_scheduler,
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.2.4 评估器构建

+

本案例训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。代码如下:

+
examples/rossler/train_enn.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "RosslerDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 32,
+        "weight_dict": {
+            key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+        },
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

SupervisedValidator 评估器与 SupervisedConstraint 比较相似,不同的是评估器需要设置评价指标 metric,在这里使用 ppsci.metric.MSE

+

3.2.5 模型训练与评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/rossler/train_enn.py
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=True,
+    validator=validator,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+
+

3.3 Transformer 模型

+

上文介绍了如何构建 Embedding 模型的训练、评估,在本节中将介绍如何使用训练好的 Embedding 模型训练 Transformer 模型。因为训练 Transformer 模型的步骤与训练 Embedding 模型的步骤基本相似,因此本节在两者的重复部分的各个参数不再详细介绍。首先将代码中定义的各个参数变量展示如下,每个参数的具体含义会在下面使用到时进行解释。

+
examples/rossler/conf/transformer.yaml
23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
# general settings
+mode: train # running mode: train/eval
+seed: 42
+output_dir: ${hydra:run.dir}
+log_freq: 20
+TRAIN_BLOCK_SIZE: 32
+VALID_BLOCK_SIZE: 256
+TRAIN_FILE_PATH: ./datasets/rossler_training.hdf5
+VALID_FILE_PATH: ./datasets/rossler_valid.hdf5
+
+# set working condition
+EMBEDDING_MODEL_PATH: ./outputs_rossler_enn/checkpoints/latest
+
+

3.3.1 约束构建

+

Transformer 模型同样基于数据驱动的方法求解问题,因此需要使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。在定义约束之前,需要首先指定监督约束中用于数据加载的各个参数,代码如下:

+
examples/rossler/train_transformer.py
64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
# manually build constraint(s)
+train_dataloader_cfg = {
+    "dataset": {
+        "name": "RosslerDataset",
+        "file_path": cfg.TRAIN_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.TRAIN_BLOCK_SIZE,
+        "stride": 16,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": True,
+        "shuffle": True,
+    },
+    "batch_size": cfg.TRAIN.batch_size,
+    "num_workers": 4,
+}
+
+

数据加载的各个参数与 Embedding 模型中的基本一致,不再赘述。需要说明的是由于 Transformer 模型训练的输入数据是 Embedding 模型 Encoder 模块的输出数据,因此我们将训练好的 Embedding 模型作为 RosslerDataset 的一个参数,在初始化时首先将训练数据映射到编码空间。

+

定义监督约束的代码如下:

+
examples/rossler/train_transformer.py
84
+85
+86
+87
+88
+89
sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    name="Sup",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.3.2 模型构建

+

在该案例中,Transformer 模型的输入输出都是编码空间中的向量,使用的 Transformer 结构如下:

+
+

rossler_transformer +

+
Transformer 网络模型
+
+

用 PaddleScience 代码表示如下:

+
examples/rossler/train_transformer.py
model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+

PhysformerGPT2 除了需要填入 input_keysoutput_keys 外,还需要设置 Transformer 模型的层数 num_layers、上下文的大小 num_ctx、输入的 Embedding 向量的长度 embed_size、多头注意力机制的参数 num_heads,在这里填入的数值为4、64、32、4。

+

3.3.3 学习率与优化器构建

+

本案例中使用的学习率方法为 CosineWarmRestarts,学习率大小设置为0.001。优化器使用 Adam,梯度裁剪使用了 Paddle 内置的 ClipGradByGlobalNorm 方法。用 PaddleScience 代码表示如下:

+
examples/rossler/train_transformer.py
# init optimizer and lr scheduler
+clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+    iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+)()
+optimizer = ppsci.optimizer.Adam(
+    lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+)(model)
+
+

3.3.4 评估器构建

+

训练过程中会按照一定的训练轮数间隔,使用验证集评估当前模型的训练情况,需要使用 SupervisedValidator 构建评估器。用 PaddleScience 代码表示如下:

+
examples/rossler/train_transformer.py
eval_dataloader_cfg = {
+    "dataset": {
+        "name": "RosslerDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+        "embedding_model": embedding_model,
+    },
+    "sampler": {
+        "name": "BatchSampler",
+        "drop_last": False,
+        "shuffle": False,
+    },
+    "batch_size": cfg.EVAL.batch_size,
+    "num_workers": 4,
+}
+
+mse_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    ppsci.loss.MSELoss(),
+    metric={"MSE": ppsci.metric.MSE()},
+    name="MSE_Validator",
+)
+validator = {mse_validator.name: mse_validator}
+
+

3.3.5 可视化器构建

+

本案例中可以通过构建可视化器在模型评估时将评估结果可视化出来,由于 Transformer 模型的输出数据是预测的编码空间的数据无法直接进行可视化,因此需要额外将输出数据使用 Embedding 网络的 Decoder 模块变换到物理状态空间。

+

在本文中首先定义了对 Transformer 模型输出数据变换到物理状态空间的代码:

+
examples/rossler/train_transformer.py
34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
def build_embedding_model(embedding_model_path: str) -> ppsci.arch.RosslerEmbedding:
+    input_keys = ("states",)
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.RosslerEmbedding(input_keys, output_keys + (regularization_key,))
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]):
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+
+        return pred_states
+
+
examples/rossler/train_transformer.py
# manually build constraint(s)
+
+

可以看到,程序首先载入了训练好的 Embedding 模型,然后在 OutputTransform__call__ 函数内实现了编码向量到物理状态空间的变换。

+

在定义好了以上代码之后,就可以实现可视化器代码的构建了:

+
examples/rossler/train_transformer.py
# set visualizer(optional)
+states = mse_validator.data_loader.dataset.data
+embedding_data = mse_validator.data_loader.dataset.embedding_data
+vis_data = {
+    "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+    "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+}
+
+visualizer = {
+    "visualize_states": ppsci.visualize.VisualizerScatter3D(
+        vis_data,
+        {
+            "pred_states": lambda d: output_transform(d),
+            "states": lambda d: d["states"],
+        },
+        num_timestamps=1,
+        prefix="result_states",
+    )
+}
+
+

首先使用上文中的 mse_validator 中的数据集进行可视化,另外还引入了 vis_data_nums 变量用于控制需要可视化样本的数量。最后通过 VisualizerScatter3D 构建可视化器。

+

3.3.6 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
examples/rossler/train_transformer.py
solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    lr_scheduler,
+    cfg.TRAIN.epochs,
+    ITERS_PER_EPOCH,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    validator=validator,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
rossler/train_enn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step1: training a embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import logger
+
+
+def get_mean_std(data: np.ndarray):
+    mean = np.asarray(
+        [np.mean(data[:, :, 0]), np.mean(data[:, :, 1]), np.min(data[:, :, 2])]
+    ).reshape(1, 3)
+    std = np.asarray(
+        [
+            np.std(data[:, :, 0]),
+            np.std(data[:, :, 1]),
+            np.max(data[:, :, 2]) - np.min(data[:, :, 2]),
+        ]
+    ).reshape(1, 3)
+    return mean, std
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    weights = (1.0 * (cfg.TRAIN_BLOCK_SIZE - 1), 1.0e3 * cfg.TRAIN_BLOCK_SIZE)
+    regularization_key = "k_matrix"
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 16,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELossWithL2Decay(
+            regularization_dict={regularization_key: 1e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)}
+        ),
+        {
+            key: lambda out, k=key: out[k]
+            for key in cfg.MODEL.output_keys + (regularization_key,)
+        },
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(sup_constraint.data_loader)
+
+    # manually init model
+    data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+    model = ppsci.arch.RosslerEmbedding(
+        cfg.MODEL.input_keys,
+        cfg.MODEL.output_keys + (regularization_key,),
+        data_mean,
+        data_std,
+    )
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay(
+        iters_per_epoch=ITERS_PER_EPOCH,
+        decay_steps=ITERS_PER_EPOCH,
+        **cfg.TRAIN.lr_scheduler,
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    weights = (1.0 * (cfg.VALID_BLOCK_SIZE - 1), 1.0e4 * cfg.VALID_BLOCK_SIZE)
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 32,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=True,
+        validator=validator,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    weights = (1.0 * (cfg.TRAIN_BLOCK_SIZE - 1), 1.0e3 * cfg.TRAIN_BLOCK_SIZE)
+    regularization_key = "k_matrix"
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 16,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELossWithL2Decay(
+            regularization_dict={regularization_key: 1e-1 * (cfg.TRAIN_BLOCK_SIZE - 1)}
+        ),
+        {
+            key: lambda out, k=key: out[k]
+            for key in cfg.MODEL.output_keys + (regularization_key,)
+        },
+        name="Sup",
+    )
+
+    # manually init model
+    data_mean, data_std = get_mean_std(sup_constraint.data_loader.dataset.data)
+    model = ppsci.arch.RosslerEmbedding(
+        cfg.MODEL.input_keys,
+        cfg.MODEL.output_keys + (regularization_key,),
+        data_mean,
+        data_std,
+    )
+
+    # manually build validator
+    weights = (1.0 * (cfg.VALID_BLOCK_SIZE - 1), 1.0e4 * cfg.VALID_BLOCK_SIZE)
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 32,
+            "weight_dict": {
+                key: value for key, value in zip(cfg.MODEL.output_keys, weights)
+            },
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="enn.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+
rossler/train_transformer.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Two-stage training
+# 1. Train a embedding model by running train_enn.py.
+# 2. Load pretrained embedding model and freeze it, then train a transformer model by running train_transformer.py.
+
+# This file is for step2: training a transformer model, based on frozen pretrained embedding model.
+# This file is based on PaddleScience/ppsci API.
+from os import path as osp
+from typing import Dict
+
+import hydra
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.arch import base
+from ppsci.utils import logger
+from ppsci.utils import save_load
+
+
+def build_embedding_model(embedding_model_path: str) -> ppsci.arch.RosslerEmbedding:
+    input_keys = ("states",)
+    output_keys = ("pred_states", "recover_states")
+    regularization_key = "k_matrix"
+    model = ppsci.arch.RosslerEmbedding(input_keys, output_keys + (regularization_key,))
+    save_load.load_pretrain(model, embedding_model_path)
+    return model
+
+
+class OutputTransform(object):
+    def __init__(self, model: base.Arch):
+        self.model = model
+        self.model.eval()
+
+    def __call__(self, x: Dict[str, paddle.Tensor]):
+        pred_embeds = x["pred_embeds"]
+        pred_states = self.model.decoder(pred_embeds)
+
+        return pred_states
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually build constraint(s)
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.TRAIN_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.TRAIN_BLOCK_SIZE,
+            "stride": 16,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": True,
+            "shuffle": True,
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "num_workers": 4,
+    }
+
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        name="Sup",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set iters_per_epoch by dataloader length
+    ITERS_PER_EPOCH = len(constraint["Sup"].data_loader)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # init optimizer and lr scheduler
+    clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=0.1)
+    lr_scheduler = ppsci.optimizer.lr_scheduler.CosineWarmRestarts(
+        iters_per_epoch=ITERS_PER_EPOCH, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer = ppsci.optimizer.Adam(
+        lr_scheduler, grad_clip=clip, **cfg.TRAIN.optimizer
+    )(model)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_data = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    visualizer = {
+        "visualize_states": ppsci.visualize.VisualizerScatter3D(
+            vis_data,
+            {
+                "pred_states": lambda d: output_transform(d),
+                "states": lambda d: d["states"],
+            },
+            num_timestamps=1,
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        lr_scheduler,
+        cfg.TRAIN.epochs,
+        ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        validator=validator,
+        visualizer=visualizer,
+    )
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    output_transform = OutputTransform(embedding_model)
+
+    # manually init model
+    model = ppsci.arch.PhysformerGPT2(**cfg.MODEL)
+
+    # manually build validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "RosslerDataset",
+            "file_path": cfg.VALID_FILE_PATH,
+            "input_keys": cfg.MODEL.input_keys,
+            "label_keys": cfg.MODEL.output_keys,
+            "block_size": cfg.VALID_BLOCK_SIZE,
+            "stride": 1024,
+            "embedding_model": embedding_model,
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "num_workers": 4,
+    }
+
+    mse_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss(),
+        metric={"MSE": ppsci.metric.MSE()},
+        name="MSE_Validator",
+    )
+    validator = {mse_validator.name: mse_validator}
+
+    # set visualizer(optional)
+    states = mse_validator.data_loader.dataset.data
+    embedding_data = mse_validator.data_loader.dataset.embedding_data
+    vis_datas = {
+        "embeds": embedding_data[: cfg.VIS_DATA_NUMS, :-1, :],
+        "states": states[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    visualizer = {
+        "visulzie_states": ppsci.visualize.VisualizerScatter3D(
+            vis_datas,
+            {
+                "pred_states": lambda d: output_transform(d),
+                "states": lambda d: d["states"],
+            },
+            num_timestamps=1,
+            prefix="result_states",
+        )
+    }
+
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        validator=validator,
+        visualizer=visualizer,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+    solver.eval()
+    # visualize prediction for pretrained model(optional)
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    # set model
+    embedding_model = build_embedding_model(cfg.EMBEDDING_MODEL_PATH)
+    model_cfg = {
+        **cfg.MODEL,
+        "embedding_model": embedding_model,
+        "input_keys": ["states"],
+        "output_keys": ["pred_states"],
+    }
+    model = ppsci.arch.PhysformerGPT2(**model_cfg)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            key: InputSpec([None, 255, 3], "float32", name=key)
+            for key in model.input_keys
+        },
+    ]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    dataset_cfg = {
+        "name": "RosslerDataset",
+        "file_path": cfg.VALID_FILE_PATH,
+        "input_keys": cfg.MODEL.input_keys,
+        "label_keys": cfg.MODEL.output_keys,
+        "block_size": cfg.VALID_BLOCK_SIZE,
+        "stride": 1024,
+    }
+
+    dataset = ppsci.data.dataset.build_dataset(dataset_cfg)
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, :-1, :],
+    }
+
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_keys = ["pred_states"]
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(output_keys, output_dict.keys())
+    }
+
+    input_dict = {
+        "states": dataset.data[: cfg.VIS_DATA_NUMS, 1:, :],
+    }
+
+    data_dict = {**input_dict, **output_dict}
+    for i in range(cfg.VIS_DATA_NUMS):
+        ppsci.visualize.save_plot_from_3d_dict(
+            f"./rossler_transformer_pred_{i}",
+            {key: value[i] for key, value in data_dict.items()},
+            ("states", "pred_states"),
+        )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="transformer.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下图中展示了两个不同初始条件下的模型预测结果和传统数值微分的预测结果。

+
+

result_states0 +

+
模型预测结果("pred_states")与传统数值微分结果("states")
+
+
+

result_states1 +

+
模型预测结果("pred_states")与传统数值微分结果("states")
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/shock_wave/index.html b/zh/examples/shock_wave/index.html new file mode 100644 index 0000000000..b6d38a96b9 --- /dev/null +++ b/zh/examples/shock_wave/index.html @@ -0,0 +1,5941 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ShockWave - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Shock Wave

+

AI Studio快速体验

+
+
+
+
+
+
+
python shock_wave.py
+
+
+
+
python shock_wave.py -cn=shock_wave_Ma0.728
+
+
+
+
+
+
+
+
+
+
python shock_wave.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/shockwave/shock_wave_Ma2_pretrained.pdparams
+
+
+
+
python shock_wave.py -cn=shock_wave_Ma0.728 mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/shockwave/shock_wave_Ma0728_pretrained.pdparams
+
+
+
+
+
+
+
+
+
+
python shock_wave.py mode=export
+
+
+
+
python shock_wave.py -cn=shock_wave_Ma0.728 mode=export
+
+
+
+
+
+
+
+
+
+
python shock_wave.py mode=infer
+
+
+
+
python shock_wave.py -cn=shock_wave_Ma0.728 mode=infer
+
+
+
+
+
+
+
+

1. 背景简介

+

激波是自然界以及工程应用中经常发现的现象。它们不仅广泛地存在于航空航天领域的可压缩流动中,而且也表现在理论与应用物理以及工程应用等其它领域。在超声速与高超声速流动中,激波的出现对流体流动的整体特征会产生重要影响。激波捕捉问题已在CFD领域发展了数十年,以弱解的数学理论为基础的激波捕捉方法以其简单易实现的特点发展迅速,并在复杂超声速、高超声速流动数值模拟中得到了广泛应用。

+

本案例针对 PINN-WE 模型进行优化,使得该模型可适用于超音速、高超音速等具有强激波的流场模拟中。

+

PINN-WE 模型通过损失函数加权,在 PINN 优化过程中减弱强梯度区域的拟合,避免了因激波区域强梯度引起的激波过拟合问题,其在一维 Euler 问题、弱激波情况下的二维问题中取得了不错的结果。但是在超音速二维流场中,该模型并没有取得很好的效果,在实验中还发现该模型经常出现激波位置偏移,激波形状不对称等非物理解的预测结果。因此本案例针对上述 PINN-WE 模型的这一问题,提出渐进加权的思想,抛弃优化过程中强调梯度思想,而是创新性地通过逐步强化梯度权重对模型优化的影响,使得模型在优化过程中能够得到较好的、符合物理的激波位置。

+

2. 问题定义

+

本问题针对二维超声速流场圆柱弓形激波进行模拟,涉及二维Euler方程,如下所示:

+
\[ +\begin{array}{cc} + \dfrac{\partial \hat{U}}{\partial t}+\dfrac{\partial \hat{F}}{\partial \xi}+\dfrac{\partial \hat{G}}{\partial \eta}=0 \\ + \text { 其中, } \quad + \begin{cases} + \hat{U}=J U \\ + \hat{F}=J\left(F \xi_x+G \xi_y\right) \\ + \hat{G}=J\left(F \eta_x+G \eta_y\right) + \end{cases} \\ + U=\left(\begin{array}{l} + \rho \\ + \rho u \\ + \rho v \\ + E + \end{array}\right), \quad F=\left(\begin{array}{l} + \rho u \\ + \rho u^2+p \\ + \rho u v \\ + (E+p) u + \end{array}\right), \quad G=\left(\begin{array}{l} + \rho v \\ + \rho v u \\ + \rho v^2+p \\ + (E+p) v + \end{array}\right) +\end{array} +\]
+

自由来流条件 \(\rho_{\infty}=1.225 \mathrm{~kg} / \mathrm{m}^3\) ; \(P_{\infty}=1 \mathrm{~atm}\)

+

整体流程如下所示:

+

computation_progress

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 ShockWave 问题中,给定时间 \(t\) 和位置坐标 \((x,y)\),模型负责预测出对应的 \(x\) 方向速度、 \(y\) 防线速度、压力、密度四个物理量 \((u,v,p,\rho)\),因此我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \((t,x,y)\)\((u,v,p,\rho)\) 的映射函数 \(g: \mathbb{R}^3 \to \mathbb{R}^4\) ,即:

+
\[ +u,v,p,\rho = g(t,x,y) +\]
+

上式中 \(g\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("t", "x", "y"),输出变量名是 ("u", "v", "p", "rho"),这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 9 层隐藏神经元,每层神经元数为 90,使用 "tanh" 作为激活函数的神经网络模型 model

+

3.2 方程构建

+

本案例涉及二维欧拉方程和边界上的方程,如下所示

+
class Euler2D(equation.PDE):
+    def __init__(self):
+        super().__init__()
+        # HACK: solver will be added here for tracking run-time epoch to
+        # compute loss factor `relu` dynamically.
+        self.solver: ppsci.solver.Solver = None
+
+        def continuity_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, rho = out["u"], out["v"], out["rho"]
+            rho__t = jacobian(rho, t)
+            rho_u = rho * u
+            rho_v = rho * v
+            rho_u__x = jacobian(rho_u, x)
+            rho_v__y = jacobian(rho_v, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            continuity = (rho__t + rho_u__x + rho_v__y) / lam
+            return continuity
+
+        self.add_equation("continuity", continuity_compute_func)
+
+        def x_momentum_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            rho_u = rho * u
+            rho_u__t = jacobian(rho_u, t)
+
+            u1 = rho * u**2 + p
+            u2 = rho * u * v
+            u1__x = jacobian(u1, x)
+            u2__y = jacobian(u2, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            x_momentum = (rho_u__t + u1__x + u2__y) / lam
+            return x_momentum
+
+        self.add_equation("x_momentum", x_momentum_compute_func)
+
+        def y_momentum_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            rho_v = rho * v
+            rho_v__t = jacobian(rho_v, t)
+
+            u2 = rho * u * v
+            u3 = rho * v**2 + p
+            u2__x = jacobian(u2, x)
+            u3__y = jacobian(u3, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            y_momentum = (rho_v__t + u2__x + u3__y) / lam
+            return y_momentum
+
+        self.add_equation("y_momentum", y_momentum_compute_func)
+
+        def energy_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            e1 = (rho * 0.5 * (u**2 + v**2) + 3.5 * p) * u
+            e2 = (rho * 0.5 * (u**2 + v**2) + 3.5 * p) * v
+            e = rho * 0.5 * (u**2 + v**2) + p / 0.4
+
+            e1__x = jacobian(e1, x)
+            e2__y = jacobian(e2, y)
+            e__t = jacobian(e, t)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            energy = (e__t + e1__x + e2__y) / lam
+            return energy
+
+        self.add_equation("energy", energy_compute_func)
+
+
+class BC_EQ(equation.PDE):
+    def __init__(self):
+        super().__init__()
+        # HACK: solver will be added here for tracking run-time epoch to
+        # compute loss factor `relu` dynamically.
+        self.solver: ppsci.solver.Solver = None
+
+        def item1_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v = out["u"], out["v"]
+            sin, cos = out["sin"], out["cos"]
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item1 = (u * cos + v * sin) / lam
+
+            return item1
+
+        self.add_equation("item1", item1_compute_func)
+
+        def item2_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v, p = out["u"], out["v"], out["p"]
+            sin, cos = out["sin"], out["cos"]
+            p__x = jacobian(p, x)
+            p__y = jacobian(p, y)
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item2 = (p__x * cos + p__y * sin) / lam
+
+            return item2
+
+        self.add_equation("item2", item2_compute_func)
+
+        def item3_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v, rho = out["u"], out["v"], out["rho"]
+            sin, cos = out["sin"], out["cos"]
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            rho__x = jacobian(rho, x)
+            rho__y = jacobian(rho, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item3 = (rho__x * cos + rho__y * sin) / lam
+
+            return item3
+
+        self.add_equation("item3", item3_compute_func)
+
+
# set equation
+equation = {"Euler2D": Euler2D(), "BC_EQ": BC_EQ()}
+
+

3.3 计算域构建

+

本案例的计算域为 0 ~ 0.4 单位时间,长为 1.5,宽为 2.0 的长方形区域,其内含有一个圆心坐标为 [1, 1],半径为 0.25 的圆,代码如下所示

+
31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
MA: 2.0
+
+# set hyper-parameters
+Lt: 0.4
+Lx: 1.5
+Ly: 2.0
+rx: 1.0
+ry: 1.0
+rd: 0.25
+N_INTERIOR: 100000
+N_BOUNDARY: 10000
+RHO1: 2.112
+P1: 3.001
+
+

3.4 约束构建

+

3.4.1 内部点约束

+

我们将欧拉方程施加在计算域的内部点上,并且使用拉丁超立方(Latin HyperCube Sampling, LHS)方法采样共 N_INTERIOR 个训练点,代码如下所示:

+
ry: 1.0
+
+
# Latin HyperCube Sampling
+# generate PDE data
+xlimits = np.array([[0.0, 0.0, 0.0], [cfg.Lt, cfg.Lx, cfg.Ly]]).T
+doe_lhs = lhs.LHS(cfg.N_INTERIOR, xlimits)
+x_int_train = doe_lhs.get_sample()
+x_int_train = x_int_train[
+    ~(
+        (x_int_train[:, 1] - cfg.rx) ** 2 + (x_int_train[:, 2] - cfg.ry) ** 2
+        < cfg.rd**2
+    )
+]
+x_int_train_dict = misc.convert_to_dict(x_int_train, cfg.MODEL.input_keys)
+
+y_int_train = np.zeros([len(x_int_train), len(cfg.MODEL.output_keys)], dtype)
+y_int_train_dict = misc.convert_to_dict(
+    y_int_train, tuple(equation["Euler2D"].equations.keys())
+)
+
+
# set constraints
+pde_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "input": x_int_train_dict,
+            "label": y_int_train_dict,
+        },
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean"),
+    output_expr=equation["Euler2D"].equations,
+    name="PDE",
+)
+
+

3.4.2 边界约束

+

我们将边界条件施加在计算域的边界点上,同样使用拉丁超立方(Latin HyperCube Sampling, LHS)方法在边界上采样共 N_BOUNDARY 个训练点,代码如下所示:

+
rd: 0.25
+
+
# generate BC data(left, right side)
+xlimits = np.array([[0.0, 0.0, 0.0], [cfg.Lt, 0.0, cfg.Ly]]).T
+doe_lhs = lhs.LHS(cfg.N_BOUNDARY, xlimits)
+x_bcL_train = doe_lhs.get_sample()
+x_bcL_train_dict = misc.convert_to_dict(x_bcL_train, cfg.MODEL.input_keys)
+
+u_bcL_train, v_bcL_train, p_bcL_train, rho_bcL_train = generate_bc_left_points(
+    x_bcL_train, cfg.MA, cfg.RHO1, cfg.P1, cfg.V1, cfg.GAMMA
+)
+y_bcL_train = np.concatenate(
+    [
+        u_bcL_train,
+        v_bcL_train,
+        p_bcL_train,
+        rho_bcL_train,
+    ],
+    axis=1,
+)
+y_bcL_train_dict = misc.convert_to_dict(
+    y_bcL_train,
+    tuple(model.output_keys),
+)
+
+x_bcI_train, sin_bcI_train, cos_bcI_train = generate_bc_down_circle_points(
+    cfg.Lt, cfg.rx, cfg.ry, cfg.rd, cfg.N_BOUNDARY
+)
+x_bcI_train_dict = misc.convert_to_dict(
+    np.concatenate([x_bcI_train, sin_bcI_train, cos_bcI_train], axis=1),
+    cfg.MODEL.input_keys + ["sin", "cos"],
+)
+y_bcI_train_dict = misc.convert_to_dict(
+    np.zeros((len(x_bcI_train), 3), dtype),
+    ("item1", "item2", "item3"),
+)
+
+
bcI_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "input": x_bcI_train_dict,
+            "label": y_bcI_train_dict,
+        },
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean", weight=10),
+    output_expr=equation["BC_EQ"].equations,
+    name="BCI",
+)
+bcL_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "input": x_bcL_train_dict,
+            "label": y_bcL_train_dict,
+        },
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean", weight=10),
+    name="BCL",
+)
+
+

3.4.3 初值约束

+

我们将边界条件施加在计算域的初始时刻的点上,同样使用拉丁超立方(Latin HyperCube Sampling, LHS)方法在初始时刻的计算域内采样共 N_BOUNDARY 个训练点,代码如下所示:

+
# generate IC data
+xlimits = np.array([[0.0, 0.0, 0.0], [0.0, cfg.Lx, cfg.Ly]]).T
+doe_lhs = lhs.LHS(cfg.N_BOUNDARY, xlimits)
+x_ic_train = doe_lhs.get_sample()
+x_ic_train = x_ic_train[
+    ~(
+        (x_ic_train[:, 1] - cfg.rx) ** 2 + (x_ic_train[:, 2] - cfg.ry) ** 2
+        < cfg.rd**2
+    )
+]
+x_ic_train_dict = misc.convert_to_dict(x_ic_train, cfg.MODEL.input_keys)
+U1 = np.sqrt(cfg.GAMMA * cfg.P1 / cfg.RHO1) * cfg.MA
+y_ic_train = np.concatenate(
+    [
+        np.full([len(x_ic_train), 1], U1, dtype),
+        np.full([len(x_ic_train), 1], 0, dtype),
+        np.full([len(x_ic_train), 1], cfg.P1, dtype),
+        np.full([len(x_ic_train), 1], cfg.RHO1, dtype),
+    ],
+    axis=1,
+)
+y_ic_train_dict = misc.convert_to_dict(
+    y_ic_train,
+    model.output_keys,
+)
+
+
ic_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "input": x_ic_train_dict,
+            "label": y_ic_train_dict,
+        },
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean", weight=10),
+    name="IC",
+)
+
+

在以上三个约束构建完毕之后,需要将他们包装成一个字典,方便后续作为参数传递

+
constraint = {
+    pde_constraint.name: pde_constraint,
+    ic_constraint.name: ic_constraint,
+    bcI_constraint.name: bcI_constraint,
+    bcL_constraint.name: bcL_constraint,
+}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用 100 轮训练轮数。

+
# training settings
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择 L-BFGS 优化器并设定 max_iter 为 100。

+
# set optimizer
+optimizer = ppsci.optimizer.LBFGS(
+    cfg.TRAIN.learning_rate, max_iter=cfg.TRAIN.max_iter
+)(model)
+
+

3.7 模型训练与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    None,
+    cfg.TRAIN.epochs,
+    cfg.TRAIN.iters_per_epoch,
+    save_freq=cfg.TRAIN.save_freq,
+    log_freq=cfg.log_freq,
+    seed=cfg.seed,
+    equation=equation,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+
+

本案例需要根据每一轮训练的 epoch 值,计算PDE、BC方程内的权重系数 relu。因此在 solver 实例化完毕之后,需额外将其传递给方程本身,代码如下:

+
# HACK: Given entire solver to euaqtion object for tracking run-time epoch
+# to compute factor `relu` dynamically.
+equation["Euler2D"].solver = solver
+equation["BC_EQ"].solver = solver
+
+

最后启动训练即可:

+
# train model
+solver.train()
+
+

训练完毕后,我们可视化最后一个时刻的计算域内辨率为 600x600 的激波,共 360000 个点,代码如下:

+
# visualize prediction
+t = np.linspace(cfg.T, cfg.T, 1)
+x = np.linspace(0.0, cfg.Lx, cfg.Nd)
+y = np.linspace(0.0, cfg.Ly, cfg.Nd)
+_, x_grid, y_grid = np.meshgrid(t, x, y)
+
+x_test = misc.cartesian_product(t, x, y)
+x_test_dict = misc.convert_to_dict(
+    x_test,
+    cfg.MODEL.input_keys,
+)
+
+output_dict = solver.predict(x_test_dict, return_numpy=True)
+u, v, p, rho = (
+    output_dict["u"],
+    output_dict["v"],
+    output_dict["p"],
+    output_dict["rho"],
+)
+
+zero_mask = (
+    (x_test[:, 1] - cfg.rx) ** 2 + (x_test[:, 2] - cfg.ry) ** 2
+) < cfg.rd**2
+u[zero_mask] = 0
+v[zero_mask] = 0
+p[zero_mask] = 0
+rho[zero_mask] = 0
+
+u = u.reshape(cfg.Nd, cfg.Nd)
+v = v.reshape(cfg.Nd, cfg.Nd)
+p = p.reshape(cfg.Nd, cfg.Nd)
+rho = rho.reshape(cfg.Nd, cfg.Nd)
+
+fig, ax = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(15, 15))
+
+plt.subplot(2, 2, 1)
+plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], u * 241.315, 60)
+plt.title("U m/s")
+plt.xlabel("x")
+plt.ylabel("y")
+axe = plt.gca()
+axe.set_aspect(1)
+plt.colorbar()
+
+plt.subplot(2, 2, 2)
+plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], v * 241.315, 60)
+plt.title("V m/s")
+plt.xlabel("x")
+plt.ylabel("y")
+axe = plt.gca()
+axe.set_aspect(1)
+plt.colorbar()
+
+plt.subplot(2, 2, 3)
+plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], p * 33775, 60)
+plt.title("P Pa")
+plt.xlabel("x")
+plt.ylabel("y")
+axe = plt.gca()
+axe.set_aspect(1)
+plt.colorbar()
+
+plt.subplot(2, 2, 4)
+plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], rho * 0.58, 60)
+plt.title("Rho kg/m^3")
+plt.xlabel("x")
+plt.ylabel("y")
+axe = plt.gca()
+axe.set_aspect(1)
+plt.colorbar()
+
+plt.savefig(osp.join(cfg.output_dir, f"shock_wave(Ma_{cfg.MA:.3f}).png"))
+
+

4. 完整代码

+
shock_wave.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+
+import hydra
+import lhs
+import numpy as np
+import paddle
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci import equation
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+from ppsci.utils import misc
+
+
+class Euler2D(equation.PDE):
+    def __init__(self):
+        super().__init__()
+        # HACK: solver will be added here for tracking run-time epoch to
+        # compute loss factor `relu` dynamically.
+        self.solver: ppsci.solver.Solver = None
+
+        def continuity_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, rho = out["u"], out["v"], out["rho"]
+            rho__t = jacobian(rho, t)
+            rho_u = rho * u
+            rho_v = rho * v
+            rho_u__x = jacobian(rho_u, x)
+            rho_v__y = jacobian(rho_v, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            continuity = (rho__t + rho_u__x + rho_v__y) / lam
+            return continuity
+
+        self.add_equation("continuity", continuity_compute_func)
+
+        def x_momentum_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            rho_u = rho * u
+            rho_u__t = jacobian(rho_u, t)
+
+            u1 = rho * u**2 + p
+            u2 = rho * u * v
+            u1__x = jacobian(u1, x)
+            u2__y = jacobian(u2, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            x_momentum = (rho_u__t + u1__x + u2__y) / lam
+            return x_momentum
+
+        self.add_equation("x_momentum", x_momentum_compute_func)
+
+        def y_momentum_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            rho_v = rho * v
+            rho_v__t = jacobian(rho_v, t)
+
+            u2 = rho * u * v
+            u3 = rho * v**2 + p
+            u2__x = jacobian(u2, x)
+            u3__y = jacobian(u3, y)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            y_momentum = (rho_v__t + u2__x + u3__y) / lam
+            return y_momentum
+
+        self.add_equation("y_momentum", y_momentum_compute_func)
+
+        def energy_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            t, x, y = out["t"], out["x"], out["y"]
+            u, v, p, rho = out["u"], out["v"], out["p"], out["rho"]
+            e1 = (rho * 0.5 * (u**2 + v**2) + 3.5 * p) * u
+            e2 = (rho * 0.5 * (u**2 + v**2) + 3.5 * p) * v
+            e = rho * 0.5 * (u**2 + v**2) + p / 0.4
+
+            e1__x = jacobian(e1, x)
+            e2__y = jacobian(e2, y)
+            e__t = jacobian(e, t)
+
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+            nab = paddle.abs(delta_u) - delta_u
+            lam = (0.1 * nab) * relu + 1
+            energy = (e__t + e1__x + e2__y) / lam
+            return energy
+
+        self.add_equation("energy", energy_compute_func)
+
+
+class BC_EQ(equation.PDE):
+    def __init__(self):
+        super().__init__()
+        # HACK: solver will be added here for tracking run-time epoch to
+        # compute loss factor `relu` dynamically.
+        self.solver: ppsci.solver.Solver = None
+
+        def item1_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v = out["u"], out["v"]
+            sin, cos = out["sin"], out["cos"]
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item1 = (u * cos + v * sin) / lam
+
+            return item1
+
+        self.add_equation("item1", item1_compute_func)
+
+        def item2_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v, p = out["u"], out["v"], out["p"]
+            sin, cos = out["sin"], out["cos"]
+            p__x = jacobian(p, x)
+            p__y = jacobian(p, y)
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item2 = (p__x * cos + p__y * sin) / lam
+
+            return item2
+
+        self.add_equation("item2", item2_compute_func)
+
+        def item3_compute_func(out):
+            relu = max(
+                0.0,
+                (self.solver.global_step // self.solver.iters_per_epoch + 1)
+                / self.solver.epochs
+                - 0.05,
+            )
+            x, y = out["x"], out["y"]
+            u, v, rho = out["u"], out["v"], out["rho"]
+            sin, cos = out["sin"], out["cos"]
+            u__x = jacobian(u, x)
+            v__y = jacobian(v, y)
+            rho__x = jacobian(rho, x)
+            rho__y = jacobian(rho, y)
+            delta_u = u__x + v__y
+
+            lam = 0.1 * (paddle.abs(delta_u) - delta_u) * relu + 1
+            item3 = (rho__x * cos + rho__y * sin) / lam
+
+            return item3
+
+        self.add_equation("item3", item3_compute_func)
+
+
+dtype = paddle.get_default_dtype()
+
+
+def generate_bc_down_circle_points(t: float, xc: float, yc: float, r: float, n: int):
+    rand_arr1 = np.random.randn(n, 1).astype(dtype)
+    theta = 2 * np.pi * rand_arr1
+    cos = np.cos(np.pi / 2 + theta)
+    sin = np.sin(np.pi / 2 + theta)
+
+    rand_arr2 = np.random.randn(n, 1).astype(dtype)
+    x = np.concatenate([rand_arr2 * t, xc + cos * r, yc + sin * r], axis=1)
+
+    return x, sin, cos
+
+
+def generate_bc_left_points(
+    x: np.ndarray, Ma: float, rho1: float, p1: float, v1: float, gamma: float
+):
+    u1: float = np.sqrt(gamma * p1 / rho1) * Ma
+    u_init = np.full((x.shape[0], 1), u1, dtype)
+    v_init = np.full((x.shape[0], 1), v1, dtype)
+    p_init = np.full((x.shape[0], 1), p1, dtype)
+    rho_init = np.full((x.shape[0], 1), rho1, dtype)
+
+    return u_init, v_init, p_init, rho_init
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"Euler2D": Euler2D(), "BC_EQ": BC_EQ()}
+
+    # Latin HyperCube Sampling
+    # generate PDE data
+    xlimits = np.array([[0.0, 0.0, 0.0], [cfg.Lt, cfg.Lx, cfg.Ly]]).T
+    doe_lhs = lhs.LHS(cfg.N_INTERIOR, xlimits)
+    x_int_train = doe_lhs.get_sample()
+    x_int_train = x_int_train[
+        ~(
+            (x_int_train[:, 1] - cfg.rx) ** 2 + (x_int_train[:, 2] - cfg.ry) ** 2
+            < cfg.rd**2
+        )
+    ]
+    x_int_train_dict = misc.convert_to_dict(x_int_train, cfg.MODEL.input_keys)
+
+    y_int_train = np.zeros([len(x_int_train), len(cfg.MODEL.output_keys)], dtype)
+    y_int_train_dict = misc.convert_to_dict(
+        y_int_train, tuple(equation["Euler2D"].equations.keys())
+    )
+
+    # generate BC data(left, right side)
+    xlimits = np.array([[0.0, 0.0, 0.0], [cfg.Lt, 0.0, cfg.Ly]]).T
+    doe_lhs = lhs.LHS(cfg.N_BOUNDARY, xlimits)
+    x_bcL_train = doe_lhs.get_sample()
+    x_bcL_train_dict = misc.convert_to_dict(x_bcL_train, cfg.MODEL.input_keys)
+
+    u_bcL_train, v_bcL_train, p_bcL_train, rho_bcL_train = generate_bc_left_points(
+        x_bcL_train, cfg.MA, cfg.RHO1, cfg.P1, cfg.V1, cfg.GAMMA
+    )
+    y_bcL_train = np.concatenate(
+        [
+            u_bcL_train,
+            v_bcL_train,
+            p_bcL_train,
+            rho_bcL_train,
+        ],
+        axis=1,
+    )
+    y_bcL_train_dict = misc.convert_to_dict(
+        y_bcL_train,
+        tuple(model.output_keys),
+    )
+
+    x_bcI_train, sin_bcI_train, cos_bcI_train = generate_bc_down_circle_points(
+        cfg.Lt, cfg.rx, cfg.ry, cfg.rd, cfg.N_BOUNDARY
+    )
+    x_bcI_train_dict = misc.convert_to_dict(
+        np.concatenate([x_bcI_train, sin_bcI_train, cos_bcI_train], axis=1),
+        cfg.MODEL.input_keys + ["sin", "cos"],
+    )
+    y_bcI_train_dict = misc.convert_to_dict(
+        np.zeros((len(x_bcI_train), 3), dtype),
+        ("item1", "item2", "item3"),
+    )
+
+    # generate IC data
+    xlimits = np.array([[0.0, 0.0, 0.0], [0.0, cfg.Lx, cfg.Ly]]).T
+    doe_lhs = lhs.LHS(cfg.N_BOUNDARY, xlimits)
+    x_ic_train = doe_lhs.get_sample()
+    x_ic_train = x_ic_train[
+        ~(
+            (x_ic_train[:, 1] - cfg.rx) ** 2 + (x_ic_train[:, 2] - cfg.ry) ** 2
+            < cfg.rd**2
+        )
+    ]
+    x_ic_train_dict = misc.convert_to_dict(x_ic_train, cfg.MODEL.input_keys)
+    U1 = np.sqrt(cfg.GAMMA * cfg.P1 / cfg.RHO1) * cfg.MA
+    y_ic_train = np.concatenate(
+        [
+            np.full([len(x_ic_train), 1], U1, dtype),
+            np.full([len(x_ic_train), 1], 0, dtype),
+            np.full([len(x_ic_train), 1], cfg.P1, dtype),
+            np.full([len(x_ic_train), 1], cfg.RHO1, dtype),
+        ],
+        axis=1,
+    )
+    y_ic_train_dict = misc.convert_to_dict(
+        y_ic_train,
+        model.output_keys,
+    )
+
+    # set constraints
+    pde_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableNamedArrayDataset",
+                "input": x_int_train_dict,
+                "label": y_int_train_dict,
+            },
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean"),
+        output_expr=equation["Euler2D"].equations,
+        name="PDE",
+    )
+    ic_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableNamedArrayDataset",
+                "input": x_ic_train_dict,
+                "label": y_ic_train_dict,
+            },
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean", weight=10),
+        name="IC",
+    )
+    bcI_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableNamedArrayDataset",
+                "input": x_bcI_train_dict,
+                "label": y_bcI_train_dict,
+            },
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean", weight=10),
+        output_expr=equation["BC_EQ"].equations,
+        name="BCI",
+    )
+    bcL_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "IterableNamedArrayDataset",
+                "input": x_bcL_train_dict,
+                "label": y_bcL_train_dict,
+            },
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean", weight=10),
+        name="BCL",
+    )
+    constraint = {
+        pde_constraint.name: pde_constraint,
+        ic_constraint.name: ic_constraint,
+        bcI_constraint.name: bcI_constraint,
+        bcL_constraint.name: bcL_constraint,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.LBFGS(
+        cfg.TRAIN.learning_rate, max_iter=cfg.TRAIN.max_iter
+    )(model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        None,
+        cfg.TRAIN.epochs,
+        cfg.TRAIN.iters_per_epoch,
+        save_freq=cfg.TRAIN.save_freq,
+        log_freq=cfg.log_freq,
+        seed=cfg.seed,
+        equation=equation,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # HACK: Given entire solver to euaqtion object for tracking run-time epoch
+    # to compute factor `relu` dynamically.
+    equation["Euler2D"].solver = solver
+    equation["BC_EQ"].solver = solver
+
+    # train model
+    solver.train()
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        seed=cfg.seed,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+    )
+
+    # visualize prediction
+    t = np.linspace(cfg.T, cfg.T, 1)
+    x = np.linspace(0.0, cfg.Lx, cfg.Nd)
+    y = np.linspace(0.0, cfg.Ly, cfg.Nd)
+    _, x_grid, y_grid = np.meshgrid(t, x, y)
+
+    x_test = misc.cartesian_product(t, x, y)
+    x_test_dict = misc.convert_to_dict(
+        x_test,
+        cfg.MODEL.input_keys,
+    )
+
+    output_dict = solver.predict(x_test_dict, return_numpy=True)
+    u, v, p, rho = (
+        output_dict["u"],
+        output_dict["v"],
+        output_dict["p"],
+        output_dict["rho"],
+    )
+
+    zero_mask = (
+        (x_test[:, 1] - cfg.rx) ** 2 + (x_test[:, 2] - cfg.ry) ** 2
+    ) < cfg.rd**2
+    u[zero_mask] = 0
+    v[zero_mask] = 0
+    p[zero_mask] = 0
+    rho[zero_mask] = 0
+
+    u = u.reshape(cfg.Nd, cfg.Nd)
+    v = v.reshape(cfg.Nd, cfg.Nd)
+    p = p.reshape(cfg.Nd, cfg.Nd)
+    rho = rho.reshape(cfg.Nd, cfg.Nd)
+
+    fig, ax = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(15, 15))
+
+    plt.subplot(2, 2, 1)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], u * 241.315, 60)
+    plt.title("U m/s")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 2)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], v * 241.315, 60)
+    plt.title("V m/s")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 3)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], p * 33775, 60)
+    plt.title("P Pa")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 4)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], rho * 0.58, 60)
+    plt.title("Rho kg/m^3")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.savefig(osp.join(cfg.output_dir, f"shock_wave(Ma_{cfg.MA:.3f}).png"))
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set models
+    model = ppsci.arch.MLP(**cfg.MODEL)
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+
+    # export models
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # visualize prediction
+    t = np.linspace(cfg.T, cfg.T, 1, dtype=np.float32)
+    x = np.linspace(0.0, cfg.Lx, cfg.Nd, dtype=np.float32)
+    y = np.linspace(0.0, cfg.Ly, cfg.Nd, dtype=np.float32)
+    _, x_grid, y_grid = np.meshgrid(t, x, y)
+
+    x_test = misc.cartesian_product(t, x, y)
+    x_test_dict = misc.convert_to_dict(
+        x_test,
+        cfg.MODEL.input_keys,
+    )
+    output_dict = predictor.predict(
+        x_test_dict,
+        cfg.INFER.batch_size,
+    )
+
+    # mapping data to cfg.MODEL.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    u, v, p, rho = (
+        output_dict["u"],
+        output_dict["v"],
+        output_dict["p"],
+        output_dict["rho"],
+    )
+
+    zero_mask = (
+        (x_test[:, 1] - cfg.rx) ** 2 + (x_test[:, 2] - cfg.ry) ** 2
+    ) < cfg.rd**2
+    u[zero_mask] = 0
+    v[zero_mask] = 0
+    p[zero_mask] = 0
+    rho[zero_mask] = 0
+
+    u = u.reshape(cfg.Nd, cfg.Nd)
+    v = v.reshape(cfg.Nd, cfg.Nd)
+    p = p.reshape(cfg.Nd, cfg.Nd)
+    rho = rho.reshape(cfg.Nd, cfg.Nd)
+
+    fig, ax = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(15, 15))
+
+    plt.subplot(2, 2, 1)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], u * 241.315, 60)
+    plt.title("U m/s")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 2)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], v * 241.315, 60)
+    plt.title("V m/s")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 3)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], p * 33775, 60)
+    plt.title("P Pa")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.subplot(2, 2, 4)
+    plt.contourf(x_grid[:, 0, :], y_grid[:, 0, :], rho * 0.58, 60)
+    plt.title("Rho kg/m^3")
+    plt.xlabel("x")
+    plt.ylabel("y")
+    axe = plt.gca()
+    axe.set_aspect(1)
+    plt.colorbar()
+
+    plt.savefig(osp.join(cfg.output_dir, f"shock_wave(Ma_{cfg.MA:.3f}).png"))
+
+
+@hydra.main(
+    version_base=None, config_path="./conf", config_name="shock_wave_Ma2.0.yaml"
+)
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

本案例针对 \(Ma=2.0\)\(Ma=0.728\) 两种不同的参数配置进行了实验,结果如下所示

+
+
+
+

+ Ma_2.0 +
Ma=2.0时,x方向速度u、y方向速度v、压力p、密度rho的预测结果
+

+
+
+

+ Ma_0.728 +
Ma=0.728时,x方向速度u、y方向速度v、压力p、密度rho的预测结果
+

+
+
+
+

6. 参考资料

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/tempoGAN/index.html b/zh/examples/tempoGAN/index.html new file mode 100644 index 0000000000..84e8a38b09 --- /dev/null +++ b/zh/examples/tempoGAN/index.html @@ -0,0 +1,7937 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tempoGAN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

tempoGAN(temporally Generative Adversarial Networks)

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_train.mat -P datasets/tempoGAN/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat -P datasets/tempoGAN/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_train.mat --create-dirs -o ./datasets/tempoGAN/2d_train.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat --create-dirs -o ./datasets/tempoGAN/2d_valid.mat
+python tempoGAN.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_train.mat -P datasets/tempoGAN/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat -P datasets/tempoGAN/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_train.mat --create-dirs -o ./datasets/tempoGAN/2d_train.mat
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat --create-dirs -o ./datasets/tempoGAN/2d_valid.mat
+python tempoGAN.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/tempoGAN/tempogan_pretrained.pdparams
+
+
+
+
python tempoGAN.py mode=export
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat -P datasets/tempoGAN/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/tempoGAN/2d_valid.mat --create-dirs -o ./datasets/tempoGAN/2d_valid.mat
+python tempoGAN.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
tempogan_pretrained.pdparamsMSE: 4.21e-5
PSNR: 47.19
SSIM: 0.9974
+

1. 背景简介

+

流体模拟方面的问题,捕捉湍流的复杂细节一直是数值模拟的长期挑战,用离散模型解决这些细节会产生巨大的计算成本,对于人类空间和时间尺度上的流动来说,很快就会变得不可行。因此流体超分辨率的需求应运而生,它旨在通过流体动力学模拟和深度学习技术将低分辨率流体模拟结果恢复为高分辨率结果,以减少生成高分辨率流体过程中的巨大计算成本。该技术可以应用于各种流体模拟,例如水流、空气流动、火焰模拟等。

+

生成式对抗网络 GAN(Generative Adversarial Networks) 是一种使用无监督学习方法的深度学习网络,GAN 网络中(至少)包含两个模型:生成器(Generator) 和判别器(Discriminator),生成器用于生成问题的输出,判别器用于判断输出的真假,两者在相互博弈中共同优化,最终使得生成器的输出接近真实值。

+

tempoGAN 在 GAN 网络的基础上新增了一个与时间相关的判别器 Discriminator_tempo,该判别器的网络结构与基础判别器相同,但输入为时间连续的几帧数据,而不是单帧数据,从而将时序纳入考虑范围。

+

本问题主要使用该网络,通过输入的低密度流体数据,得到对应的高密度流体数据,大大节省时间和计算成本。

+

2. 问题定义

+

本问题包含三个模型:生成器(Generator)、判别器(Discriminator)和与时间相关的判别器(Discriminator_tempo),根据 GAN 网络的训练流程,这三个模型交替训练,训练顺序依次为:Discriminator、Discriminator_tempo、Generator。 +GAN 网络为无监督学习,本问题网络设计中将目标值作为一个输入值,输入网络进行训练。

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集介绍

+

数据集为使用开源代码包 mantaflow 生成的 2d 流体数据集,数据集中包括一定数量连续帧的低、高密度流体图像转化成的数值,以字典的形式存储在 .mat 文件中。

+

运行本问题代码前请下载 训练数据集验证数据集, 下载后分别存放在路径:

+
output_dir: ${hydra:run.dir}
+log_freq: 20
+
+

3.2 模型构建

+
+

tempoGAN-arch +

+
tempoGAN 网络模型
+
+

上图为tempoGAN 完整的模型结构图,但本问题只针对较为简单的情况进行处理,不涉及包含速度和涡度的输入、3d、数据增强、advection operator 等部分,如果您对这些文档中未包含的内容感兴趣,可以自行修改代码并进行进一步实验。

+

如上图所示,Generator 的输入为低密度流体数据的插值,输出为生成的高密度流体模拟数据,Discriminator 的输入为低密度流体数据的插值分别与 Generator 生成的高密度流体模拟数据、目标高密度流体数据的拼接, Discriminator_tempo 的输入为多帧连续的 Generator 生成的高密度流体模拟数据以及目标高密度流体数据。

+

虽然输入输出的组成看起来较为复杂,但本质都是流体的密度数据,因此 3 个网络的映射函数都是 \(f: \mathbb{R}^1 \to \mathbb{R}^1\)

+

与简单的 MLP 网络不同,根据要解决的问题不同,GAN 的生成器和判别器有多种网络结构可以选择,在此不再赘述。由于这种独特性,本问题中的 tempoGAN 网络没有被内置在 PaddleScience 中,需要额外实现。

+

本问题中的 Generator 是一个拥有 4 层改良 Res Block 的模型,Discriminator 和 Discriminator_tempo 为同一个拥有 4 层卷积结果的模型,两者网络结构相同但输入不同。Generator、Discriminator 和 Discriminator_tempo 的网络参数也需要额外定义。

+

具体代码请参考 完整代码 中 gan.py 文件。

+

由于 GAN 网络中生成器和判别器的中间结果要相互调用,参与对方的 loss 计算,因此使用 Model List 实现,用 PaddleScience 代码表示如下:

+
57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
# define Generator model
+model_gen = ppsci.arch.Generator(**cfg.MODEL.gen_net)
+model_gen.register_input_transform(gen_funcs.transform_in)
+disc_funcs.model_gen = model_gen
+
+model_tuple = (model_gen,)
+# define Discriminators
+if cfg.USE_SPATIALDISC:
+    model_disc = ppsci.arch.Discriminator(**cfg.MODEL.disc_net)
+    model_disc.register_input_transform(disc_funcs.transform_in)
+    model_tuple += (model_disc,)
+
+# define temporal Discriminators
+if cfg.USE_TEMPODISC:
+    model_disc_tempo = ppsci.arch.Discriminator(**cfg.MODEL.tempo_net)
+    model_disc_tempo.register_input_transform(disc_funcs.transform_in_tempo)
+    model_tuple += (model_disc_tempo,)
+
+# define model_list
+model_list = ppsci.arch.ModelList(model_tuple)
+
+

注意到上述代码中定义的网络输入与实际网络输入不完全一样,因此需要对输入进行transform。

+

3.3 transform构建

+

Generator 的输入为低密度流体数据的插值,而数据集中保存的为原始的低密度流体数据,因此需要进行一个插值的 transform。

+
def transform_in(self, _in):
+    ratio = 2
+    input_dict = reshape_input(_in)
+    density_low = input_dict["density_low"]
+    density_low_inp = interpolate(density_low, ratio, "nearest")
+    return {"input_gen": density_low_inp}
+
+

Discriminator 和 Discriminator_tempo 对输入的 transform 更为复杂,分别为:

+
def transform_in(self, _in):
+    ratio = 2
+    input_dict = reshape_input(_in)
+    density_low = input_dict["density_low"]
+    density_high_from_target = input_dict["density_high"]
+
+    density_low_inp = interpolate(density_low, ratio, "nearest")
+
+    density_high_from_gen = self.model_gen(input_dict)["output_gen"]
+    density_high_from_gen.stop_gradient = True
+
+    density_input_from_target = paddle.concat(
+        [density_low_inp, density_high_from_target], axis=1
+    )
+    density_input_from_gen = paddle.concat(
+        [density_low_inp, density_high_from_gen], axis=1
+    )
+    return {
+        "input_disc_from_target": density_input_from_target,
+        "input_disc_from_gen": density_input_from_gen,
+    }
+
+def transform_in_tempo(self, _in):
+    density_high_from_target = _in["density_high"]
+
+    input_dict = reshape_input(_in)
+    density_high_from_gen = self.model_gen(input_dict)["output_gen"]
+    density_high_from_gen.stop_gradient = True
+
+    input_trans = {
+        "input_tempo_disc_from_target": density_high_from_target,
+        "input_tempo_disc_from_gen": density_high_from_gen,
+    }
+
+    return dereshape_input(input_trans, 3)
+
+

其中:

+
density_high_from_gen.stop_gradient = True
+
+

表示停止参数的计算梯度,这样设置是因为这个变量在这里仅作为 Discriminator 和 Discriminator_tempo 的输入,在反向计算时不应该参与梯度回传,如果不进行这样的设置,由于这个变量来源于 Generator 的输出,在反向传播时梯度会沿着这个变量传给 Generator,从而改变 Generator 中的参数,这显然不是我们想要的。

+

这样,我们就实例化出了一个拥有 Generator、Discriminator 和 Discriminator_tempo 并包含输入 transform 的神经网络模型 model list

+

3.4 参数和超参数设定

+

我们需要指定问题相关的参数,如数据集路径、各项 loss 的权重参数等。

+
27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
output_dir: ${hydra:run.dir}
+log_freq: 20
+DATASET_PATH: ./datasets/tempoGAN/2d_train.mat
+DATASET_PATH_VALID: ./datasets/tempoGAN/2d_valid.mat
+
+# set working condition
+USE_AMP: true
+USE_SPATIALDISC: true
+USE_TEMPODISC: true
+WEIGHT_GEN: [5.0, 0.0, 1.0]  # lambda_l1, lambda_l2, lambda_t
+WEIGHT_GEN_LAYER: [-1.0e-5, -1.0e-5, -1.0e-5, -1.0e-5, -1.0e-5]
+
+

注意到其中包含 3 个 bool 类型的变量 use_ampuse_spatialdiscuse_tempodisc,它们分别表示是否使用混合精度训练(AMP)、是否使用 Discriminator 和是否使用 Discriminator_tempo,当 use_spatialdiscuse_tempodisc 都被设置为 False 时,本问题的网络结构将会变为一个单纯的 Genrator 模型,不再是 GAN 网络了。

+

同时需要指定训练轮数和学习率等超参数,注意由于 GAN 网络训练流程与一般单个模型的网络不同,EPOCHS 的设置也有所不同。

+
73
+74
+75
+76
# training settings
+TRAIN:
+  epochs: 40000
+  epochs_gen: 1
+
+

3.5 优化器构建

+

训练使用 Adam 优化器,学习率在 Epoch 达到一半时减小到原来的 \(1/20\),因此使用 Step 方法作为学习率策略。如果将 by_epoch 设为 True,学习率将根据训练的 Epoch 改变,否则将根据 Iteration 改变。

+
78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
# initialize Adam optimizer
+lr_scheduler_gen = ppsci.optimizer.lr_scheduler.Step(
+    step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+)()
+optimizer_gen = ppsci.optimizer.Adam(lr_scheduler_gen)(model_gen)
+if cfg.USE_SPATIALDISC:
+    lr_scheduler_disc = ppsci.optimizer.lr_scheduler.Step(
+        step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer_disc = ppsci.optimizer.Adam(lr_scheduler_disc)(model_disc)
+if cfg.USE_TEMPODISC:
+    lr_scheduler_disc_tempo = ppsci.optimizer.lr_scheduler.Step(
+        step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer_disc_tempo = ppsci.optimizer.Adam(lr_scheduler_disc_tempo)(
+        (model_disc_tempo,)
+    )
+
+

3.6 约束构建

+

本问题采用无监督学习的方式,虽然不是以监督学习方式进行训练,但此处仍然可以采用监督约束 SupervisedConstraint,在定义约束之前,需要给监督约束指定文件路径等数据读取配置,因为 tempoGAN 属于自监督学习,数据集中没有标签数据,而是使用一部分输入数据作为 label,因此需要设置约束的 output_expr

+
{
+    "output_gen": lambda out: out["output_gen"],
+    "density_high": lambda out: out["density_high"],
+},
+
+

3.6.1 Generator 的约束

+

下面是约束的具体内容,要注意上述提到的 output_expr

+
sup_constraint_gen = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {
+                "density_low": dataset_train["density_low"],
+                "density_high": dataset_train["density_high"],
+            },
+            "transforms": (
+                {
+                    "FunctionalTransform": {
+                        "transform_func": data_funcs.transform,
+                    },
+                },
+            ),
+        },
+        "batch_size": cfg.TRAIN.batch_size.sup_constraint,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+    },
+    ppsci.loss.FunctionalLoss(gen_funcs.loss_func_gen),
+    {
+        "output_gen": lambda out: out["output_gen"],
+        "density_high": lambda out: out["density_high"],
+    },
+    name="sup_constraint_gen",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,其中 dataset 字段表示使用的训练数据集信息,各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 NamedArrayDataset 表示从 Array 中读取的 .mat 类型的数据集;
  2. +
  3. input: Array 类型的输入数据;
  4. +
  5. label: Array 类型的标签数据;
  6. +
  7. transforms: 所有数据 transform 方法,此处 FunctionalTransform 为PaddleScience 预留的自定义数据 transform 类,该类支持编写代码时自定义输入数据的 transform,具体代码请参考 自定义 loss 和 data transform
  8. +
+

batch_size 字段表示 batch的大小;

+

sampler 字段表示采样方法,其中各个字段表示:

+
    +
  1. name: 采样器类型,此处 BatchSampler 表示批采样器;
  2. +
  3. drop_last: 是否需要丢弃最后无法凑整一个 mini-batch 的样本,默认值为 False;
  4. +
  5. shuffle: 是否需要在生成样本下标时打乱顺序,默认值为 False;
  6. +
+

第二个参数是损失函数,此处的 FunctionalLoss 为 PaddleScience 预留的自定义 loss 函数类,该类支持编写代码时自定义 loss 的计算方法,而不是使用诸如 MSE 等现有方法,具体代码请参考 自定义 loss 和 data transform

+

第三个参数是约束条件的 output_expr,如上所述,是为了让程序可以将输入数据作为 label

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问,由于本问题设置了use_spatialdiscuse_tempodisc,导致 Generator 的部分约束不一定存在,因此先封装一定存在的约束到字典中,当其余约束存在时,在向字典中添加约束元素。

+
if cfg.USE_TEMPODISC:
+    sup_constraint_gen_tempo = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {
+                    "density_low": dataset_train["density_low_tempo"],
+                    "density_high": dataset_train["density_high_tempo"],
+                },
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": data_funcs.transform,
+                        },
+                    },
+                ),
+            },
+            "batch_size": int(cfg.TRAIN.batch_size.sup_constraint // 3),
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.FunctionalLoss(gen_funcs.loss_func_gen_tempo),
+        {
+            "output_gen": lambda out: out["output_gen"],
+            "density_high": lambda out: out["density_high"],
+        },
+        name="sup_constraint_gen_tempo",
+    )
+    constraint_gen[sup_constraint_gen_tempo.name] = sup_constraint_gen_tempo
+
+

3.6.2 Discriminator 的约束

+
if cfg.USE_SPATIALDISC:
+    sup_constraint_disc = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {
+                    "density_low": dataset_train["density_low"],
+                    "density_high": dataset_train["density_high"],
+                },
+                "label": {
+                    "out_disc_from_target": np.ones(
+                        (np.shape(dataset_train["density_high"])[0], 1),
+                        dtype=paddle.get_default_dtype(),
+                    ),
+                    "out_disc_from_gen": np.ones(
+                        (np.shape(dataset_train["density_high"])[0], 1),
+                        dtype=paddle.get_default_dtype(),
+                    ),
+                },
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": data_funcs.transform,
+                        },
+                    },
+                ),
+            },
+            "batch_size": cfg.TRAIN.batch_size.sup_constraint,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.FunctionalLoss(disc_funcs.loss_func),
+        name="sup_constraint_disc",
+    )
+    constraint_disc = {sup_constraint_disc.name: sup_constraint_disc}
+
+

各个参数含义与Generator 的约束相同。

+

3.6.3 Discriminator_tempo 的约束

+
if cfg.USE_TEMPODISC:
+    sup_constraint_disc_tempo = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {
+                    "density_low": dataset_train["density_low_tempo"],
+                    "density_high": dataset_train["density_high_tempo"],
+                },
+                "label": {
+                    "out_disc_tempo_from_target": np.ones(
+                        (np.shape(dataset_train["density_high_tempo"])[0], 1),
+                        dtype=paddle.get_default_dtype(),
+                    ),
+                    "out_disc_tempo_from_gen": np.ones(
+                        (np.shape(dataset_train["density_high_tempo"])[0], 1),
+                        dtype=paddle.get_default_dtype(),
+                    ),
+                },
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": data_funcs.transform,
+                        },
+                    },
+                ),
+            },
+            "batch_size": int(cfg.TRAIN.batch_size.sup_constraint // 3),
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.FunctionalLoss(disc_funcs.loss_func_tempo),
+        name="sup_constraint_disc_tempo",
+    )
+    constraint_disc_tempo = {
+        sup_constraint_disc_tempo.name: sup_constraint_disc_tempo
+    }
+
+

各个参数含义与Generator 的约束相同。

+

3.7 可视化器构建

+

因为 GAN 网络训练的特性,本问题不使用 PaddleScience 中内置的可视化器,而是自定义了一个用于实现推理的函数,该函数读取验证集数据,得到推理结果并将结果以图片形式保存下来,在训练过程中按照一定间隔调用该函数即可在训练过程中监控训练效果。

+
def predict_and_save_plot(
+    output_dir: str,
+    epoch_id: int,
+    solver_gen: ppsci.solver.Solver,
+    dataset_valid: np.ndarray,
+    tile_ratio: int = 1,
+):
+    """Predicting and plotting.
+
+    Args:
+        output_dir (str): Output dir path.
+        epoch_id (int): Which epoch it is.
+        solver_gen (ppsci.solver.Solver): Solver for predicting.
+        dataset_valid (np.ndarray): Valid dataset.
+        tile_ratio (int, optional): How many tiles of one dim. Defaults to 1.
+    """
+    dir_pred = "predict/"
+    os.makedirs(os.path.join(output_dir, dir_pred), exist_ok=True)
+
+    start_idx = 190
+    density_low = dataset_valid["density_low"][start_idx : start_idx + 3]
+    density_high = dataset_valid["density_high"][start_idx : start_idx + 3]
+
+    # tile
+    density_low = (
+        split_data(density_low, tile_ratio) if tile_ratio != 1 else density_low
+    )
+    density_high = (
+        split_data(density_high, tile_ratio) if tile_ratio != 1 else density_high
+    )
+
+    pred_dict = solver_gen.predict(
+        {
+            "density_low": density_low,
+            "density_high": density_high,
+        },
+        {"density_high": lambda out: out["output_gen"]},
+        batch_size=tile_ratio * tile_ratio if tile_ratio != 1 else 3,
+        no_grad=False,
+    )
+    if epoch_id == 1:
+        # plot interpolated input image
+        input_img = np.expand_dims(dataset_valid["density_low"][start_idx], axis=0)
+        input_img = paddle.to_tensor(input_img, dtype=paddle.get_default_dtype())
+        input_img = F.interpolate(
+            input_img,
+            [input_img.shape[-2] * 4, input_img.shape[-1] * 4],
+            mode="nearest",
+        ).numpy()
+        Img.imsave(
+            os.path.join(output_dir, dir_pred, "input.png"),
+            np.squeeze(input_img),
+            vmin=0.0,
+            vmax=1.0,
+            cmap="gray",
+        )
+        # plot target image
+        Img.imsave(
+            os.path.join(output_dir, dir_pred, "target.png"),
+            np.squeeze(dataset_valid["density_high"][start_idx]),
+            vmin=0.0,
+            vmax=1.0,
+            cmap="gray",
+        )
+    # plot pred image
+    pred_img = (
+        concat_data(pred_dict["density_high"].numpy(), tile_ratio)
+        if tile_ratio != 1
+        else np.squeeze(pred_dict["density_high"][0].numpy())
+    )
+    Img.imsave(
+        os.path.join(output_dir, dir_pred, f"pred_epoch_{str(epoch_id)}.png"),
+        pred_img,
+        vmin=0.0,
+        vmax=1.0,
+        cmap="gray",
+    )
+
+

3.8 自定义 loss 和 data transform

+

由于本问题采用无监督学习,数据中不存在标签数据,loss 为计算得到,因此需要自定义 loss 。方法为先定义相关函数,再将函数名作为参数传给 FunctionalLoss。需要注意自定义 loss 函数的输入输出参数需要与 PaddleScience 中如 MSE 等其他函数保持一致,即输入为模型输出 output_dict 等字典变量,输出为 loss 值 paddle.Tensor

+

3.8.1 Generator 的 loss

+

Generator 的 loss 提供了 l1 loss、l2 loss、输出经过 Discriminator 判断的 loss 和 输出经过 Discriminator_tempo 判断的 loss。这些 loss 是否存在根据权重参数控制,若某一项 loss 的权重参数为 0,则表示训练中不添加该 loss 项。

+
def loss_func_gen(self, output_dict: Dict, *args) -> paddle.Tensor:
+    """Calculate loss of generator when use spatial discriminator.
+        The loss consists of l1 loss, l2 loss and layer loss when use spatial discriminator.
+        Notice that all item of loss is optional because weight of them might be 0.
+
+    Args:
+        output_dict (Dict): output dict of model.
+
+    Returns:
+        paddle.Tensor: Loss of generator.
+    """
+    # l1 loss
+    loss_l1 = F.l1_loss(
+        output_dict["output_gen"], output_dict["density_high"], "mean"
+    )
+    losses = loss_l1 * self.weight_gen[0]
+
+    # l2 loss
+    loss_l2 = F.mse_loss(
+        output_dict["output_gen"], output_dict["density_high"], "mean"
+    )
+    losses += loss_l2 * self.weight_gen[1]
+
+    if self.weight_gen_layer is not None:
+        # disc(generator_out) loss
+        out_disc_from_gen = output_dict["out_disc_from_gen"][-1]
+        label_ones = paddle.ones_like(out_disc_from_gen)
+        loss_gen = F.binary_cross_entropy_with_logits(
+            out_disc_from_gen, label_ones, reduction="mean"
+        )
+        losses += loss_gen
+
+        # layer loss
+        key_list = list(output_dict.keys())
+        # ["out0_layer0","out0_layer1","out0_layer2","out0_layer3","out_disc_from_target",
+        # "out1_layer0","out1_layer1","out1_layer2","out1_layer3","out_disc_from_gen"]
+        loss_layer = 0
+        for i in range(1, len(self.weight_gen_layer)):
+            # i = 0,1,2,3
+            loss_layer += (
+                self.weight_gen_layer[i]
+                * F.mse_loss(
+                    output_dict[key_list[i]],
+                    output_dict[key_list[5 + i]],
+                    reduction="sum",
+                )
+                / 2
+            )
+        losses += loss_layer * self.weight_gen_layer[0]
+
+    return {"output_gen": losses}
+
+def loss_func_gen_tempo(self, output_dict: Dict, *args) -> paddle.Tensor:
+    """Calculate loss of generator when use temporal discriminator.
+        The loss is cross entropy loss when use temporal discriminator.
+
+    Args:
+        output_dict (Dict): output dict of model.
+
+    Returns:
+        paddle.Tensor: Loss of generator.
+    """
+    out_disc_tempo_from_gen = output_dict["out_disc_tempo_from_gen"][-1]
+    label_t_ones = paddle.ones_like(out_disc_tempo_from_gen)
+
+    loss_gen_t = F.binary_cross_entropy_with_logits(
+        out_disc_tempo_from_gen, label_t_ones, reduction="mean"
+    )
+    losses = loss_gen_t * self.weight_gen[2]
+    return {"out_disc_tempo_from_gen": losses}
+
+

3.8.2 Discriminator 的 loss

+

Discriminator 为判别器,它的作用是判断数据为真数据还是假数据,因此它的 loss 为 Generator 产生的数据应当判断为假而产生的 loss 和 目标值数据应当判断为真而产生的 loss。

+
def loss_func(self, output_dict, *args):
+    out_disc_from_target = output_dict["out_disc_from_target"]
+    out_disc_from_gen = output_dict["out_disc_from_gen"]
+
+    label_ones = paddle.ones_like(out_disc_from_target)
+    label_zeros = paddle.zeros_like(out_disc_from_gen)
+
+    loss_disc_from_target = F.binary_cross_entropy_with_logits(
+        out_disc_from_target, label_ones, reduction="mean"
+    )
+    loss_disc_from_gen = F.binary_cross_entropy_with_logits(
+        out_disc_from_gen, label_zeros, reduction="mean"
+    )
+    losses = loss_disc_from_target * self.weight_disc + loss_disc_from_gen
+    return losses
+
+

3.8.3 Discriminator_tempo 的 loss

+

Discriminator_tempo 的 loss 构成 与 Discriminator 相同,只是所需数据不同。

+
def loss_func_tempo(self, output_dict, *args):
+    out_disc_tempo_from_target = output_dict["out_disc_tempo_from_target"]
+    out_disc_tempo_from_gen = output_dict["out_disc_tempo_from_gen"]
+
+    label_ones = paddle.ones_like(out_disc_tempo_from_target)
+    label_zeros = paddle.zeros_like(out_disc_tempo_from_gen)
+
+    loss_disc_tempo_from_target = F.binary_cross_entropy_with_logits(
+        out_disc_tempo_from_target, label_ones, reduction="mean"
+    )
+    loss_disc_tempo_from_gen = F.binary_cross_entropy_with_logits(
+        out_disc_tempo_from_gen, label_zeros, reduction="mean"
+    )
+    losses = (
+        loss_disc_tempo_from_target * self.weight_disc + loss_disc_tempo_from_gen
+    )
+    return losses
+
+

3.8.4 自定义 data transform

+

本问题提供了一种输入数据处理方法,将输入的流体密度数据随机裁剪一块,然后进行密度值判断,若裁剪下来的块密度值低于阈值则重新裁剪,直到密度满足条件或裁剪次数达到阈值。这样做主要是为了减少训练所需的显存,同时对裁剪下来的块密度值的判断保证了块中信息的丰富程度。参数和超参数设定tile_ratio 表示原始尺寸是块的尺寸的几倍,即若tile_ratio 为 2,裁剪下来的块的大小为整张原始图片的四分之一。

+
class DataFuncs:
+    """All functions used for data transform.
+
+    Args:
+        tile_ratio (int, optional): How many tiles of one dim. Defaults to 1.
+        density_min (float, optional): Minimize density of one tile. Defaults to 0.02.
+        max_turn (int, optional): Maximize turn of taking a tile from one image. Defaults to 20.
+    """
+
+    def __init__(
+        self, tile_ratio: int = 1, density_min: float = 0.02, max_turn: int = 20
+    ) -> None:
+        self.tile_ratio = tile_ratio
+        self.density_min = density_min
+        self.max_turn = max_turn
+
+    def transform(
+        self,
+        input_item: Dict[str, np.ndarray],
+        label_item: Dict[str, np.ndarray],
+        weight_item: Dict[str, np.ndarray],
+    ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray]]:
+        if self.tile_ratio == 1:
+            return input_item, label_item, weight_item
+        for _ in range(self.max_turn):
+            rand_ratio = np.random.rand()
+            density_low = self.cut_data(input_item["density_low"], rand_ratio)
+            density_high = self.cut_data(input_item["density_high"], rand_ratio)
+            if self.is_valid_tile(density_low):
+                break
+
+        input_item["density_low"] = density_low
+        input_item["density_high"] = density_high
+        return input_item, label_item, weight_item
+
+    def cut_data(self, data: np.ndarray, rand_ratio: float) -> paddle.Tensor:
+        # data: C,H,W
+        _, H, W = data.shape
+        if H % self.tile_ratio != 0 or W % self.tile_ratio != 0:
+            exit(
+                f"ERROR: input images cannot be divided into {self.tile_ratio} parts evenly!"
+            )
+        tile_shape = [H // self.tile_ratio, W // self.tile_ratio]
+        rand_shape = np.floor(rand_ratio * (np.array([H, W]) - np.array(tile_shape)))
+        start = [int(rand_shape[0]), int(rand_shape[1])]
+        end = [int(rand_shape[0] + tile_shape[0]), int(rand_shape[1] + tile_shape[1])]
+        data = paddle.slice(
+            paddle.to_tensor(data), axes=[-2, -1], starts=start, ends=end
+        )
+
+        return data
+
+    def is_valid_tile(self, tile: paddle.Tensor):
+        img_density = tile[0].sum()
+        return img_density >= (
+            self.density_min * tile.shape[0] * tile.shape[1] * tile.shape[2]
+        )
+
+

注意,此处代码仅提供 data transform 的思路。当前代码中简单的分块方法由于输入包含的信息变少,显然会影响训练效果,因此本问题中当显存充足时,应当将 tile_ratio 设置为 1,当显存不足时,也建议优先考虑使用混合精度训练来减少现存占用。

+

3.9 模型训练

+

完成上述设置之后,首先需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
solver_gen = ppsci.solver.Solver(
+    model_list,
+    constraint_gen,
+    cfg.output_dir,
+    optimizer_gen,
+    lr_scheduler_gen,
+    cfg.TRAIN.epochs_gen,
+    cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    use_amp=cfg.USE_AMP,
+    amp_level=cfg.TRAIN.amp_level,
+)
+
+

注意 GAN 类型的网络训练方法为多个模型交替训练,与单一模型或多模型分阶段训练不同,不能简单的使用 solver.train API,具体代码请参考 完整代码 中 tempoGAN.py 文件。

+

3.10 模型评估

+

3.10.1 训练中评估

+

训练中仅在特定 Epoch 保存特定图片的目标结果和模型输出结果,训练结束后针对最后一个 Epoch 的输出结果进行一次评估,以便直观评价模型优化效果。不使用 PaddleScience 中内置的评估器,也不在训练过程中进行评估:

+
for i in range(1, cfg.TRAIN.epochs + 1):
+    logger.message(f"\nEpoch: {i}\n")
+    # plotting during training
+    if i == 1 or i % PRED_INTERVAL == 0 or i == cfg.TRAIN.epochs:
+        func_module.predict_and_save_plot(
+            cfg.output_dir, i, solver_gen, dataset_valid, cfg.TILE_RATIO
+        )
+
+
############### evaluation for training ###############
+img_target = (
+    func_module.get_image_array(
+        os.path.join(cfg.output_dir, "predict", "target.png")
+    )
+    / 255.0
+)
+img_pred = (
+    func_module.get_image_array(
+        os.path.join(
+            cfg.output_dir, "predict", f"pred_epoch_{cfg.TRAIN.epochs}.png"
+        )
+    )
+    / 255.0
+)
+eval_mse, eval_psnr, eval_ssim = func_module.evaluate_img(img_target, img_pred)
+logger.message(f"MSE: {eval_mse}, PSNR: {eval_psnr}, SSIM: {eval_ssim}")
+
+

具体代码请参考 完整代码 中 tempoGAN.py 文件。

+

3.10.2 eval 中评估

+

本问题的评估指标为,将模型输出的超分结果与实际高分辨率图片做对比,使用三个指标 MSE(Mean-Square Error) 、PSNR(Peak Signal-to-Noise Ratio) 、SSIM(Structural SIMilarity) 来评价图片相似度。因此没有使用 PaddleScience 中的内置评估器,也没有 Solver.eval() 过程。

+
def evaluate(cfg: DictConfig):
+    if cfg.EVAL.save_outs:
+        from matplotlib import image as Img
+
+        os.makedirs(osp.join(cfg.output_dir, "eval_outs"), exist_ok=True)
+
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    gen_funcs = func_module.GenFuncs(cfg.WEIGHT_GEN, None)
+
+    # load dataset
+    dataset_valid = hdf5storage.loadmat(cfg.DATASET_PATH_VALID)
+
+    # define Generator model
+    model_gen = ppsci.arch.Generator(**cfg.MODEL.gen_net)
+    model_gen.register_input_transform(gen_funcs.transform_in)
+
+    # define model_list
+    model_list = ppsci.arch.ModelList((model_gen,))
+
+    # load pretrained model
+    save_load.load_pretrain(model_list, cfg.EVAL.pretrained_model_path)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {
+                "density_low": dataset_valid["density_low"],
+            },
+            "label": {"density_high": dataset_valid["density_high"]},
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": 1,
+    }
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss("mean"),
+        {"density_high": lambda out: out["output_gen"]},
+        metric={"metric": ppsci.metric.L2Rel()},
+        name="sup_validator_gen",
+    )
+
+    # customized evalution
+    def scale(data):
+        smax = np.max(data)
+        smin = np.min(data)
+        return (data - smin) / (smax - smin)
+
+    eval_mse_list = []
+    eval_psnr_list = []
+    eval_ssim_list = []
+    for i, (input, label, _) in enumerate(sup_validator.data_loader):
+        output_dict = model_list({"density_low": input["density_low"]})
+        output_arr = scale(np.squeeze(output_dict["output_gen"].numpy()))
+        target_arr = scale(np.squeeze(label["density_high"].numpy()))
+
+        eval_mse, eval_psnr, eval_ssim = func_module.evaluate_img(
+            target_arr, output_arr
+        )
+        eval_mse_list.append(eval_mse)
+        eval_psnr_list.append(eval_psnr)
+        eval_ssim_list.append(eval_ssim)
+
+        if cfg.EVAL.save_outs:
+            Img.imsave(
+                osp.join(cfg.output_dir, "eval_outs", f"out_{i}.png"),
+                output_arr,
+                vmin=0.0,
+                vmax=1.0,
+                cmap="gray",
+            )
+    logger.message(
+        f"MSE: {np.mean(eval_mse_list)}, PSNR: {np.mean(eval_psnr_list)}, SSIM: {np.mean(eval_ssim_list)}"
+    )
+
+

另外,其中:

+
if cfg.EVAL.save_outs:
+    Img.imsave(
+        osp.join(cfg.output_dir, "eval_outs", f"out_{i}.png"),
+        output_arr,
+        vmin=0.0,
+        vmax=1.0,
+        cmap="gray",
+    )
+
+

提供了保存模型输出结果的选择,以便更直观的看出超分后的结果,是否开启由配置文件 EVAL 中的 save_outs 指定:

+
91
+92
+93
+94
  checkpoint_path: null
+
+# evaluation settings
+EVAL:
+
+

4. 完整代码

+

完整代码包含 PaddleScience 具体训练流程代码 tempoGAN.py 和所有自定义函数代码 functions.py,另外还向 ppsci.arch 添加了网络结构代码 gan.py,一并显示在下面,如果需要自定义网络结构,可以作为参考。

+
tempoGAN.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from os import path as osp
+
+import functions as func_module
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.utils import checker
+from ppsci.utils import logger
+from ppsci.utils import save_load
+
+if not checker.dynamic_import_to_globals("hdf5storage"):
+    raise ImportError(
+        "Could not import hdf5storage python package. "
+        "Please install it with `pip install hdf5storage`."
+    )
+import hdf5storage
+
+
+def train(cfg: DictConfig):
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    gen_funcs = func_module.GenFuncs(
+        cfg.WEIGHT_GEN, (cfg.WEIGHT_GEN_LAYER if cfg.USE_SPATIALDISC else None)
+    )
+    disc_funcs = func_module.DiscFuncs(cfg.WEIGHT_DISC)
+    data_funcs = func_module.DataFuncs(cfg.TILE_RATIO)
+
+    # load dataset
+    logger.message(
+        "Attention! Start loading datasets, this will take tens of seconds to several minutes, please wait patiently."
+    )
+    dataset_train = hdf5storage.loadmat(cfg.DATASET_PATH)
+    logger.message("Finish loading training dataset.")
+    dataset_valid = hdf5storage.loadmat(cfg.DATASET_PATH_VALID)
+    logger.message("Finish loading validation dataset.")
+
+    # define Generator model
+    model_gen = ppsci.arch.Generator(**cfg.MODEL.gen_net)
+    model_gen.register_input_transform(gen_funcs.transform_in)
+    disc_funcs.model_gen = model_gen
+
+    model_tuple = (model_gen,)
+    # define Discriminators
+    if cfg.USE_SPATIALDISC:
+        model_disc = ppsci.arch.Discriminator(**cfg.MODEL.disc_net)
+        model_disc.register_input_transform(disc_funcs.transform_in)
+        model_tuple += (model_disc,)
+
+    # define temporal Discriminators
+    if cfg.USE_TEMPODISC:
+        model_disc_tempo = ppsci.arch.Discriminator(**cfg.MODEL.tempo_net)
+        model_disc_tempo.register_input_transform(disc_funcs.transform_in_tempo)
+        model_tuple += (model_disc_tempo,)
+
+    # define model_list
+    model_list = ppsci.arch.ModelList(model_tuple)
+
+    # initialize Adam optimizer
+    lr_scheduler_gen = ppsci.optimizer.lr_scheduler.Step(
+        step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+    )()
+    optimizer_gen = ppsci.optimizer.Adam(lr_scheduler_gen)(model_gen)
+    if cfg.USE_SPATIALDISC:
+        lr_scheduler_disc = ppsci.optimizer.lr_scheduler.Step(
+            step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+        )()
+        optimizer_disc = ppsci.optimizer.Adam(lr_scheduler_disc)(model_disc)
+    if cfg.USE_TEMPODISC:
+        lr_scheduler_disc_tempo = ppsci.optimizer.lr_scheduler.Step(
+            step_size=cfg.TRAIN.epochs // 2, **cfg.TRAIN.lr_scheduler
+        )()
+        optimizer_disc_tempo = ppsci.optimizer.Adam(lr_scheduler_disc_tempo)(
+            (model_disc_tempo,)
+        )
+
+    # Generator
+    # manually build constraint(s)
+    sup_constraint_gen = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {
+                    "density_low": dataset_train["density_low"],
+                    "density_high": dataset_train["density_high"],
+                },
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": data_funcs.transform,
+                        },
+                    },
+                ),
+            },
+            "batch_size": cfg.TRAIN.batch_size.sup_constraint,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": False,
+            },
+        },
+        ppsci.loss.FunctionalLoss(gen_funcs.loss_func_gen),
+        {
+            "output_gen": lambda out: out["output_gen"],
+            "density_high": lambda out: out["density_high"],
+        },
+        name="sup_constraint_gen",
+    )
+    constraint_gen = {sup_constraint_gen.name: sup_constraint_gen}
+    if cfg.USE_TEMPODISC:
+        sup_constraint_gen_tempo = ppsci.constraint.SupervisedConstraint(
+            {
+                "dataset": {
+                    "name": "NamedArrayDataset",
+                    "input": {
+                        "density_low": dataset_train["density_low_tempo"],
+                        "density_high": dataset_train["density_high_tempo"],
+                    },
+                    "transforms": (
+                        {
+                            "FunctionalTransform": {
+                                "transform_func": data_funcs.transform,
+                            },
+                        },
+                    ),
+                },
+                "batch_size": int(cfg.TRAIN.batch_size.sup_constraint // 3),
+                "sampler": {
+                    "name": "BatchSampler",
+                    "drop_last": False,
+                    "shuffle": False,
+                },
+            },
+            ppsci.loss.FunctionalLoss(gen_funcs.loss_func_gen_tempo),
+            {
+                "output_gen": lambda out: out["output_gen"],
+                "density_high": lambda out: out["density_high"],
+            },
+            name="sup_constraint_gen_tempo",
+        )
+        constraint_gen[sup_constraint_gen_tempo.name] = sup_constraint_gen_tempo
+
+    # Discriminators
+    # manually build constraint(s)
+    if cfg.USE_SPATIALDISC:
+        sup_constraint_disc = ppsci.constraint.SupervisedConstraint(
+            {
+                "dataset": {
+                    "name": "NamedArrayDataset",
+                    "input": {
+                        "density_low": dataset_train["density_low"],
+                        "density_high": dataset_train["density_high"],
+                    },
+                    "label": {
+                        "out_disc_from_target": np.ones(
+                            (np.shape(dataset_train["density_high"])[0], 1),
+                            dtype=paddle.get_default_dtype(),
+                        ),
+                        "out_disc_from_gen": np.ones(
+                            (np.shape(dataset_train["density_high"])[0], 1),
+                            dtype=paddle.get_default_dtype(),
+                        ),
+                    },
+                    "transforms": (
+                        {
+                            "FunctionalTransform": {
+                                "transform_func": data_funcs.transform,
+                            },
+                        },
+                    ),
+                },
+                "batch_size": cfg.TRAIN.batch_size.sup_constraint,
+                "sampler": {
+                    "name": "BatchSampler",
+                    "drop_last": False,
+                    "shuffle": False,
+                },
+            },
+            ppsci.loss.FunctionalLoss(disc_funcs.loss_func),
+            name="sup_constraint_disc",
+        )
+        constraint_disc = {sup_constraint_disc.name: sup_constraint_disc}
+
+    # temporal Discriminators
+    # manually build constraint(s)
+    if cfg.USE_TEMPODISC:
+        sup_constraint_disc_tempo = ppsci.constraint.SupervisedConstraint(
+            {
+                "dataset": {
+                    "name": "NamedArrayDataset",
+                    "input": {
+                        "density_low": dataset_train["density_low_tempo"],
+                        "density_high": dataset_train["density_high_tempo"],
+                    },
+                    "label": {
+                        "out_disc_tempo_from_target": np.ones(
+                            (np.shape(dataset_train["density_high_tempo"])[0], 1),
+                            dtype=paddle.get_default_dtype(),
+                        ),
+                        "out_disc_tempo_from_gen": np.ones(
+                            (np.shape(dataset_train["density_high_tempo"])[0], 1),
+                            dtype=paddle.get_default_dtype(),
+                        ),
+                    },
+                    "transforms": (
+                        {
+                            "FunctionalTransform": {
+                                "transform_func": data_funcs.transform,
+                            },
+                        },
+                    ),
+                },
+                "batch_size": int(cfg.TRAIN.batch_size.sup_constraint // 3),
+                "sampler": {
+                    "name": "BatchSampler",
+                    "drop_last": False,
+                    "shuffle": False,
+                },
+            },
+            ppsci.loss.FunctionalLoss(disc_funcs.loss_func_tempo),
+            name="sup_constraint_disc_tempo",
+        )
+        constraint_disc_tempo = {
+            sup_constraint_disc_tempo.name: sup_constraint_disc_tempo
+        }
+
+    # initialize solver
+    solver_gen = ppsci.solver.Solver(
+        model_list,
+        constraint_gen,
+        cfg.output_dir,
+        optimizer_gen,
+        lr_scheduler_gen,
+        cfg.TRAIN.epochs_gen,
+        cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        use_amp=cfg.USE_AMP,
+        amp_level=cfg.TRAIN.amp_level,
+    )
+    if cfg.USE_SPATIALDISC:
+        solver_disc = ppsci.solver.Solver(
+            model_list,
+            constraint_disc,
+            cfg.output_dir,
+            optimizer_disc,
+            lr_scheduler_disc,
+            cfg.TRAIN.epochs_disc,
+            cfg.TRAIN.iters_per_epoch,
+            eval_during_train=cfg.TRAIN.eval_during_train,
+            use_amp=cfg.USE_AMP,
+            amp_level=cfg.TRAIN.amp_level,
+        )
+    if cfg.USE_TEMPODISC:
+        solver_disc_tempo = ppsci.solver.Solver(
+            model_list,
+            constraint_disc_tempo,
+            cfg.output_dir,
+            optimizer_disc_tempo,
+            lr_scheduler_disc_tempo,
+            cfg.TRAIN.epochs_disc_tempo,
+            cfg.TRAIN.iters_per_epoch,
+            eval_during_train=cfg.TRAIN.eval_during_train,
+            use_amp=cfg.USE_AMP,
+            amp_level=cfg.TRAIN.amp_level,
+        )
+
+    PRED_INTERVAL = 200
+    for i in range(1, cfg.TRAIN.epochs + 1):
+        logger.message(f"\nEpoch: {i}\n")
+        # plotting during training
+        if i == 1 or i % PRED_INTERVAL == 0 or i == cfg.TRAIN.epochs:
+            func_module.predict_and_save_plot(
+                cfg.output_dir, i, solver_gen, dataset_valid, cfg.TILE_RATIO
+            )
+
+        disc_funcs.model_gen = model_gen
+        # train disc, input: (x,y,G(x))
+        if cfg.USE_SPATIALDISC:
+            solver_disc.train()
+
+        # train disc tempo, input: (y_3,G(x)_3)
+        if cfg.USE_TEMPODISC:
+            solver_disc_tempo.train()
+
+        # train gen, input: (x,)
+        solver_gen.train()
+
+    ############### evaluation for training ###############
+    img_target = (
+        func_module.get_image_array(
+            os.path.join(cfg.output_dir, "predict", "target.png")
+        )
+        / 255.0
+    )
+    img_pred = (
+        func_module.get_image_array(
+            os.path.join(
+                cfg.output_dir, "predict", f"pred_epoch_{cfg.TRAIN.epochs}.png"
+            )
+        )
+        / 255.0
+    )
+    eval_mse, eval_psnr, eval_ssim = func_module.evaluate_img(img_target, img_pred)
+    logger.message(f"MSE: {eval_mse}, PSNR: {eval_psnr}, SSIM: {eval_ssim}")
+
+
+def evaluate(cfg: DictConfig):
+    if cfg.EVAL.save_outs:
+        from matplotlib import image as Img
+
+        os.makedirs(osp.join(cfg.output_dir, "eval_outs"), exist_ok=True)
+
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    gen_funcs = func_module.GenFuncs(cfg.WEIGHT_GEN, None)
+
+    # load dataset
+    dataset_valid = hdf5storage.loadmat(cfg.DATASET_PATH_VALID)
+
+    # define Generator model
+    model_gen = ppsci.arch.Generator(**cfg.MODEL.gen_net)
+    model_gen.register_input_transform(gen_funcs.transform_in)
+
+    # define model_list
+    model_list = ppsci.arch.ModelList((model_gen,))
+
+    # load pretrained model
+    save_load.load_pretrain(model_list, cfg.EVAL.pretrained_model_path)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {
+                "density_low": dataset_valid["density_low"],
+            },
+            "label": {"density_high": dataset_valid["density_high"]},
+        },
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": False,
+        },
+        "batch_size": 1,
+    }
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        ppsci.loss.MSELoss("mean"),
+        {"density_high": lambda out: out["output_gen"]},
+        metric={"metric": ppsci.metric.L2Rel()},
+        name="sup_validator_gen",
+    )
+
+    # customized evalution
+    def scale(data):
+        smax = np.max(data)
+        smin = np.min(data)
+        return (data - smin) / (smax - smin)
+
+    eval_mse_list = []
+    eval_psnr_list = []
+    eval_ssim_list = []
+    for i, (input, label, _) in enumerate(sup_validator.data_loader):
+        output_dict = model_list({"density_low": input["density_low"]})
+        output_arr = scale(np.squeeze(output_dict["output_gen"].numpy()))
+        target_arr = scale(np.squeeze(label["density_high"].numpy()))
+
+        eval_mse, eval_psnr, eval_ssim = func_module.evaluate_img(
+            target_arr, output_arr
+        )
+        eval_mse_list.append(eval_mse)
+        eval_psnr_list.append(eval_psnr)
+        eval_ssim_list.append(eval_ssim)
+
+        if cfg.EVAL.save_outs:
+            Img.imsave(
+                osp.join(cfg.output_dir, "eval_outs", f"out_{i}.png"),
+                output_arr,
+                vmin=0.0,
+                vmax=1.0,
+                cmap="gray",
+            )
+    logger.message(
+        f"MSE: {np.mean(eval_mse_list)}, PSNR: {np.mean(eval_psnr_list)}, SSIM: {np.mean(eval_ssim_list)}"
+    )
+
+
+def export(cfg: DictConfig):
+    from paddle.static import InputSpec
+
+    # set models
+    gen_funcs = func_module.GenFuncs(cfg.WEIGHT_GEN, None)
+    model_gen = ppsci.arch.Generator(**cfg.MODEL.gen_net)
+    model_gen.register_input_transform(gen_funcs.transform_in)
+
+    # define model_list
+    model_list = ppsci.arch.ModelList((model_gen,))
+
+    # load pretrained model
+    solver = ppsci.solver.Solver(
+        model=model_list, pretrained_model_path=cfg.INFER.pretrained_model_path
+    )
+
+    # export models
+    input_spec = [
+        {"density_low": InputSpec([None, 1, 128, 128], "float32", name="density_low")},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path, skip_prune_program=True)
+
+
+def inference(cfg: DictConfig):
+    from matplotlib import image as Img
+
+    from deploy.python_infer import pinn_predictor
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # load dataset
+    dataset_infer = {
+        "density_low": hdf5storage.loadmat(cfg.DATASET_PATH_VALID)["density_low"]
+    }
+
+    output_dict = predictor.predict(dataset_infer, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output = [output_dict[key] for key in output_dict]
+
+    def scale(data):
+        smax = np.max(data)
+        smin = np.min(data)
+        return (data - smin) / (smax - smin)
+
+    for i, img in enumerate(output[0]):
+        img = scale(np.squeeze(img))
+        Img.imsave(
+            osp.join(cfg.output_dir, f"out_{i}.png"),
+            img,
+            vmin=0.0,
+            vmax=1.0,
+            cmap="gray",
+        )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="tempogan.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
functions.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from typing import Dict
+from typing import List
+from typing import Tuple
+
+import numpy as np
+import paddle
+import paddle.nn.functional as F
+from matplotlib import image as Img
+from PIL import Image
+from skimage.metrics import mean_squared_error
+from skimage.metrics import peak_signal_noise_ratio
+from skimage.metrics import structural_similarity
+
+import ppsci
+from ppsci.utils import logger
+
+
+# train
+def interpolate(
+    data: paddle.Tensor, ratio: int, mode: str = "nearest"
+) -> paddle.Tensor:
+    """Interpolate twice.
+
+    Args:
+        data (paddle.Tensor): The data to be interpolated.
+        ratio (int): Ratio of one interpolation.
+        mode (str, optional): Interpolation method. Defaults to "nearest".
+
+    Returns:
+        paddle.Tensor: Data interpolated.
+    """
+    for _ in range(2):
+        data = F.interpolate(
+            data,
+            [data.shape[-2] * ratio, data.shape[-1] * ratio],
+            mode=mode,
+        )
+    return data
+
+
+def reshape_input(input_dict: Dict[str, paddle.Tensor]) -> Dict[str, paddle.Tensor]:
+    """Reshape input data for temporally Discriminator. Reshape data from N, C, W, H to N * C, 1, H, W.
+        Which will merge N dimension and C dimension to 1 dimension but still keep 4 dimensions
+        to ensure the data can be used for training.
+
+    Args:
+        input_dict (Dict[str, paddle.Tensor]): input data dict.
+
+    Returns:
+        Dict[str, paddle.Tensor]: reshaped data dict.
+    """
+    for key in input_dict:
+        input = input_dict[key]
+        N, C, H, W = input.shape
+        input_dict[key] = paddle.reshape(input, [N * C, 1, H, W])
+    return input_dict
+
+
+def dereshape_input(
+    input_dict: Dict[str, paddle.Tensor], C: int
+) -> Dict[str, paddle.Tensor]:
+    """Dereshape input data for temporally Discriminator. Deeshape data from N * C, 1, H, W to N, C, W, H.
+
+    Args:
+        input_dict (Dict[str, paddle.Tensor]): input data dict.
+        C (int): Channel of dereshape.
+
+    Returns:
+        Dict[str, paddle.Tensor]: dereshaped data dict.
+    """
+    for key in input_dict:
+        input = input_dict[key]
+        N, _, H, W = input.shape
+        if N < C:
+            logger.warning(
+                f"batch_size is smaller than {C}! Tempo needs at least {C} frames, input will be copied."
+            )
+            input_dict[key] = paddle.concat([input[:1]] * C, axis=1)
+        else:
+            N_new = int(N // C)
+            input_dict[key] = paddle.reshape(input[: N_new * C], [-1, C, H, W])
+    return input_dict
+
+
+# predict
+def split_data(data: np.ndarray, tile_ratio: int) -> np.ndarray:
+    """Split a numpy image to tiles equally.
+
+    Args:
+        data (np.ndarray): The image to be Split.
+        tile_ratio (int): How many tiles of one dim.
+            Number of result tiles is tile_ratio * tile_ratio for a 2d image.
+
+    Returns:
+        np.ndarray: Tiles in [N,C,H,W] shape.
+    """
+    _, _, h, w = data.shape
+    tile_h, tile_w = h // tile_ratio, w // tile_ratio
+    tiles = []
+    for i in range(tile_ratio):
+        for j in range(tile_ratio):
+            tiles.append(
+                data[
+                    :1,
+                    :,
+                    i * tile_h : i * tile_h + tile_h,
+                    j * tile_w : j * tile_w + tile_w,
+                ],
+            )
+    return np.concatenate(tiles, axis=0)
+
+
+def concat_data(data: np.ndarray, tile_ratio: int) -> np.ndarray:
+    """Concat numpy tiles to a image equally.
+
+    Args:
+        data (np.ndarray): The tiles to be upsplited.
+        tile_ratio (int): How many tiles of one dim.
+            Number of input tiles is tile_ratio * tile_ratio for 2d result.
+
+    Returns:
+        np.ndarray: Image in [H,W] shape.
+    """
+    _, _, tile_h, tile_w = data.shape
+    h, w = tile_h * tile_ratio, tile_w * tile_ratio
+    data_whole = np.ones([h, w], dtype=paddle.get_default_dtype())
+    tile_idx = 0
+    for i in range(tile_ratio):
+        for j in range(tile_ratio):
+            data_whole[
+                i * tile_h : i * tile_h + tile_h,
+                j * tile_w : j * tile_w + tile_w,
+            ] = data[tile_idx][0]
+            tile_idx += 1
+    return data_whole
+
+
+def predict_and_save_plot(
+    output_dir: str,
+    epoch_id: int,
+    solver_gen: ppsci.solver.Solver,
+    dataset_valid: np.ndarray,
+    tile_ratio: int = 1,
+):
+    """Predicting and plotting.
+
+    Args:
+        output_dir (str): Output dir path.
+        epoch_id (int): Which epoch it is.
+        solver_gen (ppsci.solver.Solver): Solver for predicting.
+        dataset_valid (np.ndarray): Valid dataset.
+        tile_ratio (int, optional): How many tiles of one dim. Defaults to 1.
+    """
+    dir_pred = "predict/"
+    os.makedirs(os.path.join(output_dir, dir_pred), exist_ok=True)
+
+    start_idx = 190
+    density_low = dataset_valid["density_low"][start_idx : start_idx + 3]
+    density_high = dataset_valid["density_high"][start_idx : start_idx + 3]
+
+    # tile
+    density_low = (
+        split_data(density_low, tile_ratio) if tile_ratio != 1 else density_low
+    )
+    density_high = (
+        split_data(density_high, tile_ratio) if tile_ratio != 1 else density_high
+    )
+
+    pred_dict = solver_gen.predict(
+        {
+            "density_low": density_low,
+            "density_high": density_high,
+        },
+        {"density_high": lambda out: out["output_gen"]},
+        batch_size=tile_ratio * tile_ratio if tile_ratio != 1 else 3,
+        no_grad=False,
+    )
+    if epoch_id == 1:
+        # plot interpolated input image
+        input_img = np.expand_dims(dataset_valid["density_low"][start_idx], axis=0)
+        input_img = paddle.to_tensor(input_img, dtype=paddle.get_default_dtype())
+        input_img = F.interpolate(
+            input_img,
+            [input_img.shape[-2] * 4, input_img.shape[-1] * 4],
+            mode="nearest",
+        ).numpy()
+        Img.imsave(
+            os.path.join(output_dir, dir_pred, "input.png"),
+            np.squeeze(input_img),
+            vmin=0.0,
+            vmax=1.0,
+            cmap="gray",
+        )
+        # plot target image
+        Img.imsave(
+            os.path.join(output_dir, dir_pred, "target.png"),
+            np.squeeze(dataset_valid["density_high"][start_idx]),
+            vmin=0.0,
+            vmax=1.0,
+            cmap="gray",
+        )
+    # plot pred image
+    pred_img = (
+        concat_data(pred_dict["density_high"].numpy(), tile_ratio)
+        if tile_ratio != 1
+        else np.squeeze(pred_dict["density_high"][0].numpy())
+    )
+    Img.imsave(
+        os.path.join(output_dir, dir_pred, f"pred_epoch_{str(epoch_id)}.png"),
+        pred_img,
+        vmin=0.0,
+        vmax=1.0,
+        cmap="gray",
+    )
+
+
+# evaluation
+def evaluate_img(
+    img_target: np.ndarray, img_pred: np.ndarray
+) -> Tuple[float, float, float]:
+    """Evaluate two images.
+
+    Args:
+        img_target (np.ndarray): Target image.
+        img_pred (np.ndarray): Image generated by prediction.
+
+    Returns:
+        Tuple[float, float, float]: MSE, PSNR, SSIM.
+    """
+    eval_mse = mean_squared_error(img_target, img_pred)
+    eval_psnr = peak_signal_noise_ratio(img_target, img_pred)
+    eval_ssim = structural_similarity(img_target, img_pred, data_range=1.0)
+    return eval_mse, eval_psnr, eval_ssim
+
+
+def get_image_array(img_path):
+    return np.array(Image.open(img_path).convert("L"))
+
+
+class GenFuncs:
+    """All functions used for Generator, including functions of transform and loss.
+
+    Args:
+        weight_gen (List[float]): Weights of L1 loss.
+        weight_gen_layer (List[float], optional): Weights of layers loss. Defaults to None.
+    """
+
+    def __init__(
+        self, weight_gen: List[float], weight_gen_layer: List[float] = None
+    ) -> None:
+        self.weight_gen = weight_gen
+        self.weight_gen_layer = weight_gen_layer
+
+    def transform_in(self, _in):
+        ratio = 2
+        input_dict = reshape_input(_in)
+        density_low = input_dict["density_low"]
+        density_low_inp = interpolate(density_low, ratio, "nearest")
+        return {"input_gen": density_low_inp}
+
+    def loss_func_gen(self, output_dict: Dict, *args) -> paddle.Tensor:
+        """Calculate loss of generator when use spatial discriminator.
+            The loss consists of l1 loss, l2 loss and layer loss when use spatial discriminator.
+            Notice that all item of loss is optional because weight of them might be 0.
+
+        Args:
+            output_dict (Dict): output dict of model.
+
+        Returns:
+            paddle.Tensor: Loss of generator.
+        """
+        # l1 loss
+        loss_l1 = F.l1_loss(
+            output_dict["output_gen"], output_dict["density_high"], "mean"
+        )
+        losses = loss_l1 * self.weight_gen[0]
+
+        # l2 loss
+        loss_l2 = F.mse_loss(
+            output_dict["output_gen"], output_dict["density_high"], "mean"
+        )
+        losses += loss_l2 * self.weight_gen[1]
+
+        if self.weight_gen_layer is not None:
+            # disc(generator_out) loss
+            out_disc_from_gen = output_dict["out_disc_from_gen"][-1]
+            label_ones = paddle.ones_like(out_disc_from_gen)
+            loss_gen = F.binary_cross_entropy_with_logits(
+                out_disc_from_gen, label_ones, reduction="mean"
+            )
+            losses += loss_gen
+
+            # layer loss
+            key_list = list(output_dict.keys())
+            # ["out0_layer0","out0_layer1","out0_layer2","out0_layer3","out_disc_from_target",
+            # "out1_layer0","out1_layer1","out1_layer2","out1_layer3","out_disc_from_gen"]
+            loss_layer = 0
+            for i in range(1, len(self.weight_gen_layer)):
+                # i = 0,1,2,3
+                loss_layer += (
+                    self.weight_gen_layer[i]
+                    * F.mse_loss(
+                        output_dict[key_list[i]],
+                        output_dict[key_list[5 + i]],
+                        reduction="sum",
+                    )
+                    / 2
+                )
+            losses += loss_layer * self.weight_gen_layer[0]
+
+        return {"output_gen": losses}
+
+    def loss_func_gen_tempo(self, output_dict: Dict, *args) -> paddle.Tensor:
+        """Calculate loss of generator when use temporal discriminator.
+            The loss is cross entropy loss when use temporal discriminator.
+
+        Args:
+            output_dict (Dict): output dict of model.
+
+        Returns:
+            paddle.Tensor: Loss of generator.
+        """
+        out_disc_tempo_from_gen = output_dict["out_disc_tempo_from_gen"][-1]
+        label_t_ones = paddle.ones_like(out_disc_tempo_from_gen)
+
+        loss_gen_t = F.binary_cross_entropy_with_logits(
+            out_disc_tempo_from_gen, label_t_ones, reduction="mean"
+        )
+        losses = loss_gen_t * self.weight_gen[2]
+        return {"out_disc_tempo_from_gen": losses}
+
+
+class DiscFuncs:
+    """All functions used for Discriminator and temporally Discriminator, including functions of transform and loss.
+
+    Args:
+        weight_disc (float): Weight of loss generated by the discriminator to judge the true target.
+    """
+
+    def __init__(self, weight_disc: float) -> None:
+        self.weight_disc = weight_disc
+        self.model_gen = None
+
+    def transform_in(self, _in):
+        ratio = 2
+        input_dict = reshape_input(_in)
+        density_low = input_dict["density_low"]
+        density_high_from_target = input_dict["density_high"]
+
+        density_low_inp = interpolate(density_low, ratio, "nearest")
+
+        density_high_from_gen = self.model_gen(input_dict)["output_gen"]
+        density_high_from_gen.stop_gradient = True
+
+        density_input_from_target = paddle.concat(
+            [density_low_inp, density_high_from_target], axis=1
+        )
+        density_input_from_gen = paddle.concat(
+            [density_low_inp, density_high_from_gen], axis=1
+        )
+        return {
+            "input_disc_from_target": density_input_from_target,
+            "input_disc_from_gen": density_input_from_gen,
+        }
+
+    def transform_in_tempo(self, _in):
+        density_high_from_target = _in["density_high"]
+
+        input_dict = reshape_input(_in)
+        density_high_from_gen = self.model_gen(input_dict)["output_gen"]
+        density_high_from_gen.stop_gradient = True
+
+        input_trans = {
+            "input_tempo_disc_from_target": density_high_from_target,
+            "input_tempo_disc_from_gen": density_high_from_gen,
+        }
+
+        return dereshape_input(input_trans, 3)
+
+    def loss_func(self, output_dict, *args):
+        out_disc_from_target = output_dict["out_disc_from_target"]
+        out_disc_from_gen = output_dict["out_disc_from_gen"]
+
+        label_ones = paddle.ones_like(out_disc_from_target)
+        label_zeros = paddle.zeros_like(out_disc_from_gen)
+
+        loss_disc_from_target = F.binary_cross_entropy_with_logits(
+            out_disc_from_target, label_ones, reduction="mean"
+        )
+        loss_disc_from_gen = F.binary_cross_entropy_with_logits(
+            out_disc_from_gen, label_zeros, reduction="mean"
+        )
+        losses = loss_disc_from_target * self.weight_disc + loss_disc_from_gen
+        return losses
+
+    def loss_func_tempo(self, output_dict, *args):
+        out_disc_tempo_from_target = output_dict["out_disc_tempo_from_target"]
+        out_disc_tempo_from_gen = output_dict["out_disc_tempo_from_gen"]
+
+        label_ones = paddle.ones_like(out_disc_tempo_from_target)
+        label_zeros = paddle.zeros_like(out_disc_tempo_from_gen)
+
+        loss_disc_tempo_from_target = F.binary_cross_entropy_with_logits(
+            out_disc_tempo_from_target, label_ones, reduction="mean"
+        )
+        loss_disc_tempo_from_gen = F.binary_cross_entropy_with_logits(
+            out_disc_tempo_from_gen, label_zeros, reduction="mean"
+        )
+        losses = (
+            loss_disc_tempo_from_target * self.weight_disc + loss_disc_tempo_from_gen
+        )
+        return losses
+
+
+class DataFuncs:
+    """All functions used for data transform.
+
+    Args:
+        tile_ratio (int, optional): How many tiles of one dim. Defaults to 1.
+        density_min (float, optional): Minimize density of one tile. Defaults to 0.02.
+        max_turn (int, optional): Maximize turn of taking a tile from one image. Defaults to 20.
+    """
+
+    def __init__(
+        self, tile_ratio: int = 1, density_min: float = 0.02, max_turn: int = 20
+    ) -> None:
+        self.tile_ratio = tile_ratio
+        self.density_min = density_min
+        self.max_turn = max_turn
+
+    def transform(
+        self,
+        input_item: Dict[str, np.ndarray],
+        label_item: Dict[str, np.ndarray],
+        weight_item: Dict[str, np.ndarray],
+    ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray]]:
+        if self.tile_ratio == 1:
+            return input_item, label_item, weight_item
+        for _ in range(self.max_turn):
+            rand_ratio = np.random.rand()
+            density_low = self.cut_data(input_item["density_low"], rand_ratio)
+            density_high = self.cut_data(input_item["density_high"], rand_ratio)
+            if self.is_valid_tile(density_low):
+                break
+
+        input_item["density_low"] = density_low
+        input_item["density_high"] = density_high
+        return input_item, label_item, weight_item
+
+    def cut_data(self, data: np.ndarray, rand_ratio: float) -> paddle.Tensor:
+        # data: C,H,W
+        _, H, W = data.shape
+        if H % self.tile_ratio != 0 or W % self.tile_ratio != 0:
+            exit(
+                f"ERROR: input images cannot be divided into {self.tile_ratio} parts evenly!"
+            )
+        tile_shape = [H // self.tile_ratio, W // self.tile_ratio]
+        rand_shape = np.floor(rand_ratio * (np.array([H, W]) - np.array(tile_shape)))
+        start = [int(rand_shape[0]), int(rand_shape[1])]
+        end = [int(rand_shape[0] + tile_shape[0]), int(rand_shape[1] + tile_shape[1])]
+        data = paddle.slice(
+            paddle.to_tensor(data), axes=[-2, -1], starts=start, ends=end
+        )
+
+        return data
+
+    def is_valid_tile(self, tile: paddle.Tensor):
+        img_density = tile[0].sum()
+        return img_density >= (
+            self.density_min * tile.shape[0] * tile.shape[1] * tile.shape[2]
+        )
+
+
gan.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+from typing import Dict
+from typing import List
+from typing import Tuple
+
+import paddle
+import paddle.nn as nn
+
+from ppsci.arch import activation as act_mod
+from ppsci.arch import base
+
+
+class Conv2DBlock(nn.Layer):
+    def __init__(
+        self,
+        in_channel,
+        out_channel,
+        kernel_size,
+        stride,
+        use_bn,
+        act,
+        mean,
+        std,
+        value,
+    ):
+        super().__init__()
+        weight_attr = paddle.ParamAttr(
+            initializer=nn.initializer.Normal(mean=mean, std=std)
+        )
+        bias_attr = paddle.ParamAttr(initializer=nn.initializer.Constant(value=value))
+        self.conv_2d = nn.Conv2D(
+            in_channel,
+            out_channel,
+            kernel_size,
+            stride,
+            padding="SAME",
+            weight_attr=weight_attr,
+            bias_attr=bias_attr,
+        )
+        self.bn = nn.BatchNorm2D(out_channel) if use_bn else None
+        self.act = act_mod.get_activation(act) if act else None
+
+    def forward(self, x):
+        y = x
+        y = self.conv_2d(y)
+        if self.bn:
+            y = self.bn(y)
+        if self.act:
+            y = self.act(y)
+        return y
+
+
+class VariantResBlock(nn.Layer):
+    def __init__(
+        self,
+        in_channel,
+        out_channels,
+        kernel_sizes,
+        strides,
+        use_bns,
+        acts,
+        mean,
+        std,
+        value,
+    ):
+        super().__init__()
+        self.conv_2d_0 = Conv2DBlock(
+            in_channel=in_channel,
+            out_channel=out_channels[0],
+            kernel_size=kernel_sizes[0],
+            stride=strides[0],
+            use_bn=use_bns[0],
+            act=acts[0],
+            mean=mean,
+            std=std,
+            value=value,
+        )
+        self.conv_2d_1 = Conv2DBlock(
+            in_channel=out_channels[0],
+            out_channel=out_channels[1],
+            kernel_size=kernel_sizes[1],
+            stride=strides[1],
+            use_bn=use_bns[1],
+            act=acts[1],
+            mean=mean,
+            std=std,
+            value=value,
+        )
+
+        self.conv_2d_2 = Conv2DBlock(
+            in_channel=in_channel,
+            out_channel=out_channels[2],
+            kernel_size=kernel_sizes[2],
+            stride=strides[2],
+            use_bn=use_bns[2],
+            act=acts[2],
+            mean=mean,
+            std=std,
+            value=value,
+        )
+
+        self.act = act_mod.get_activation("relu")
+
+    def forward(self, x):
+        y = x
+        y = self.conv_2d_0(y)
+        y = self.conv_2d_1(y)
+        short = self.conv_2d_2(x)
+        y = paddle.add(y, short)
+        y = self.act(y)
+        return y
+
+
+class FCBlock(nn.Layer):
+    def __init__(self, in_channel, act, mean, std, value):
+        super().__init__()
+        self.flatten = nn.Flatten()
+        weight_attr = paddle.ParamAttr(
+            initializer=nn.initializer.Normal(mean=mean, std=std)
+        )
+        bias_attr = paddle.ParamAttr(initializer=nn.initializer.Constant(value=value))
+        self.linear = nn.Linear(
+            in_channel,
+            1,
+            weight_attr=weight_attr,
+            bias_attr=bias_attr,
+        )
+        self.act = act_mod.get_activation(act) if act else None
+
+    def forward(self, x):
+        y = x
+        y = self.flatten(y)
+        y = self.linear(y)
+        if self.act:
+            y = self.act(y)
+        return y
+
+
+class Generator(base.Arch):
+    """Generator Net of GAN. Attention, the net using a kind of variant of ResBlock which is
+        unique to "tempoGAN" example but not an open source network.
+
+    Args:
+        input_keys (Tuple[str, ...]): Name of input keys, such as ("input1", "input2").
+        output_keys (Tuple[str, ...]): Name of output keys, such as ("output1", "output2").
+        in_channel (int): Number of input channels of the first conv layer.
+        out_channels_tuple (Tuple[Tuple[int, ...], ...]): Number of output channels of all conv layers,
+            such as [[out_res0_conv0, out_res0_conv1], [out_res1_conv0, out_res1_conv1]]
+        kernel_sizes_tuple (Tuple[Tuple[int, ...], ...]): Number of kernel_size of all conv layers,
+            such as [[kernel_size_res0_conv0, kernel_size_res0_conv1], [kernel_size_res1_conv0, kernel_size_res1_conv1]]
+        strides_tuple (Tuple[Tuple[int, ...], ...]): Number of stride of all conv layers,
+            such as [[stride_res0_conv0, stride_res0_conv1], [stride_res1_conv0, stride_res1_conv1]]
+        use_bns_tuple (Tuple[Tuple[bool, ...], ...]): Whether to use the batch_norm layer after each conv layer.
+        acts_tuple (Tuple[Tuple[str, ...], ...]): Whether to use the activation layer after each conv layer. If so, witch activation to use,
+            such as [[act_res0_conv0, act_res0_conv1], [act_res1_conv0, act_res1_conv1]]
+
+    Examples:
+        >>> import ppsci
+        >>> in_channel = 1
+        >>> rb_channel0 = (2, 8, 8)
+        >>> rb_channel1 = (128, 128, 128)
+        >>> rb_channel2 = (32, 8, 8)
+        >>> rb_channel3 = (2, 1, 1)
+        >>> out_channels_tuple = (rb_channel0, rb_channel1, rb_channel2, rb_channel3)
+        >>> kernel_sizes_tuple = (((5, 5), ) * 2 + ((1, 1), ), ) * 4
+        >>> strides_tuple = ((1, 1, 1), ) * 4
+        >>> use_bns_tuple = ((True, True, True), ) * 3 + ((False, False, False), )
+        >>> acts_tuple = (("relu", None, None), ) * 4
+        >>> model = ppsci.arch.Generator(("in",), ("out",), in_channel, out_channels_tuple, kernel_sizes_tuple, strides_tuple, use_bns_tuple, acts_tuple)
+        >>> batch_size = 4
+        >>> height = 64
+        >>> width = 64
+        >>> input_data = paddle.randn([batch_size, in_channel, height, width])
+        >>> input_dict = {'in': input_data}
+        >>> output_data = model(input_dict)
+        >>> print(output_data['out'].shape)
+        [4, 1, 64, 64]
+    """
+
+    def __init__(
+        self,
+        input_keys: Tuple[str, ...],
+        output_keys: Tuple[str, ...],
+        in_channel: int,
+        out_channels_tuple: Tuple[Tuple[int, ...], ...],
+        kernel_sizes_tuple: Tuple[Tuple[int, ...], ...],
+        strides_tuple: Tuple[Tuple[int, ...], ...],
+        use_bns_tuple: Tuple[Tuple[bool, ...], ...],
+        acts_tuple: Tuple[Tuple[str, ...], ...],
+    ):
+        super().__init__()
+        self.input_keys = input_keys
+        self.output_keys = output_keys
+        self.in_channel = in_channel
+        self.out_channels_tuple = out_channels_tuple
+        self.kernel_sizes_tuple = kernel_sizes_tuple
+        self.strides_tuple = strides_tuple
+        self.use_bns_tuple = use_bns_tuple
+        self.acts_tuple = acts_tuple
+
+        self.init_blocks()
+
+    def init_blocks(self):
+        blocks_list = []
+        for i in range(len(self.out_channels_tuple)):
+            in_channel = (
+                self.in_channel if i == 0 else self.out_channels_tuple[i - 1][-1]
+            )
+            blocks_list.append(
+                VariantResBlock(
+                    in_channel=in_channel,
+                    out_channels=self.out_channels_tuple[i],
+                    kernel_sizes=self.kernel_sizes_tuple[i],
+                    strides=self.strides_tuple[i],
+                    use_bns=self.use_bns_tuple[i],
+                    acts=self.acts_tuple[i],
+                    mean=0.0,
+                    std=0.04,
+                    value=0.1,
+                )
+            )
+        self.blocks = nn.LayerList(blocks_list)
+
+    def forward_tensor(self, x):
+        y = x
+        for block in self.blocks:
+            y = block(y)
+        return y
+
+    def forward(self, x):
+        if self._input_transform is not None:
+            x = self._input_transform(x)
+
+        y = self.concat_to_tensor(x, self.input_keys, axis=-1)
+        y = self.forward_tensor(y)
+        y = self.split_to_dict(y, self.output_keys, axis=-1)
+
+        if self._output_transform is not None:
+            y = self._output_transform(x, y)
+        return y
+
+
+class Discriminator(base.Arch):
+    """Discriminator Net of GAN.
+
+    Args:
+        input_keys (Tuple[str, ...]): Name of input keys, such as ("input1", "input2").
+        output_keys (Tuple[str, ...]): Name of output keys, such as ("output1", "output2").
+        in_channel (int):  Number of input channels of the first conv layer.
+        out_channels (Tuple[int, ...]): Number of output channels of all conv layers,
+            such as (out_conv0, out_conv1, out_conv2).
+        fc_channel (int):  Number of input features of linear layer. Number of output features of the layer
+            is set to 1 in this Net to construct a fully_connected layer.
+        kernel_sizes (Tuple[int, ...]): Number of kernel_size of all conv layers,
+            such as (kernel_size_conv0, kernel_size_conv1, kernel_size_conv2).
+        strides (Tuple[int, ...]): Number of stride of all conv layers,
+            such as (stride_conv0, stride_conv1, stride_conv2).
+        use_bns (Tuple[bool, ...]): Whether to use the batch_norm layer after each conv layer.
+        acts (Tuple[str, ...]): Whether to use the activation layer after each conv layer. If so, witch activation to use,
+            such as (act_conv0, act_conv1, act_conv2).
+
+    Examples:
+        >>> import ppsci
+        >>> in_channel = 2
+        >>> in_channel_tempo = 3
+        >>> out_channels = (32, 64, 128, 256)
+        >>> fc_channel = 65536
+        >>> kernel_sizes = ((4, 4), (4, 4), (4, 4), (4, 4))
+        >>> strides = (2, 2, 2, 1)
+        >>> use_bns = (False, True, True, True)
+        >>> acts = ("leaky_relu", "leaky_relu", "leaky_relu", "leaky_relu", None)
+        >>> output_keys_disc = ("out_1", "out_2", "out_3", "out_4", "out_5", "out_6", "out_7", "out_8", "out_9", "out_10")
+        >>> model = ppsci.arch.Discriminator(("in_1","in_2"), output_keys_disc, in_channel, out_channels, fc_channel, kernel_sizes, strides, use_bns, acts)
+        >>> input_data = [paddle.to_tensor(paddle.randn([1, in_channel, 128, 128])),paddle.to_tensor(paddle.randn([1, in_channel, 128, 128]))]
+        >>> input_dict = {"in_1": input_data[0],"in_2": input_data[1]}
+        >>> out_dict = model(input_dict)
+        >>> for k, v in out_dict.items():
+        ...     print(k, v.shape)
+        out_1 [1, 32, 64, 64]
+        out_2 [1, 64, 32, 32]
+        out_3 [1, 128, 16, 16]
+        out_4 [1, 256, 16, 16]
+        out_5 [1, 1]
+        out_6 [1, 32, 64, 64]
+        out_7 [1, 64, 32, 32]
+        out_8 [1, 128, 16, 16]
+        out_9 [1, 256, 16, 16]
+        out_10 [1, 1]
+    """
+
+    def __init__(
+        self,
+        input_keys: Tuple[str, ...],
+        output_keys: Tuple[str, ...],
+        in_channel: int,
+        out_channels: Tuple[int, ...],
+        fc_channel: int,
+        kernel_sizes: Tuple[int, ...],
+        strides: Tuple[int, ...],
+        use_bns: Tuple[bool, ...],
+        acts: Tuple[str, ...],
+    ):
+        super().__init__()
+        self.input_keys = input_keys
+        self.output_keys = output_keys
+        self.in_channel = in_channel
+        self.out_channels = out_channels
+        self.fc_channel = fc_channel
+        self.kernel_sizes = kernel_sizes
+        self.strides = strides
+        self.use_bns = use_bns
+        self.acts = acts
+
+        self.init_layers()
+
+    def init_layers(self):
+        layers_list = []
+        for i in range(len(self.out_channels)):
+            in_channel = self.in_channel if i == 0 else self.out_channels[i - 1]
+            layers_list.append(
+                Conv2DBlock(
+                    in_channel=in_channel,
+                    out_channel=self.out_channels[i],
+                    kernel_size=self.kernel_sizes[i],
+                    stride=self.strides[i],
+                    use_bn=self.use_bns[i],
+                    act=self.acts[i],
+                    mean=0.0,
+                    std=0.04,
+                    value=0.1,
+                )
+            )
+
+        layers_list.append(
+            FCBlock(self.fc_channel, self.acts[4], mean=0.0, std=0.04, value=0.1)
+        )
+        self.layers = nn.LayerList(layers_list)
+
+    def forward_tensor(self, x):
+        y = x
+        y_list = []
+        for layer in self.layers:
+            y = layer(y)
+            y_list.append(y)
+        return y_list  # y_conv1, y_conv2, y_conv3, y_conv4, y_fc(y_out)
+
+    def forward(self, x):
+        if self._input_transform is not None:
+            x = self._input_transform(x)
+
+        y_list = []
+        # y1_conv1, y1_conv2, y1_conv3, y1_conv4, y1_fc, y2_conv1, y2_conv2, y2_conv3, y2_conv4, y2_fc
+        for k in x:
+            y_list.extend(self.forward_tensor(x[k]))
+
+        y = self.split_to_dict(y_list, self.output_keys)
+
+        if self._output_transform is not None:
+            y = self._output_transform(x, y)
+
+        return y
+
+    @staticmethod
+    def split_to_dict(
+        data_list: List[paddle.Tensor], keys: Tuple[str, ...]
+    ) -> Dict[str, paddle.Tensor]:
+        """Overwrite of split_to_dict() method belongs to Class base.Arch.
+
+        Reason for overwriting is there is no concat_to_tensor() method called in "tempoGAN" example.
+        That is because input in "tempoGAN" example is not in a regular format, but a format like:
+        {
+            "input1": paddle.concat([in1, in2], axis=1),
+            "input2": paddle.concat([in1, in3], axis=1),
+        }
+
+        Args:
+            data_list (List[paddle.Tensor]): The data to be split. It should be a list of tensor(s), but not a paddle.Tensor.
+            keys (Tuple[str, ...]): Keys of outputs.
+
+        Returns:
+            Dict[str, paddle.Tensor]: Dict with split data.
+        """
+        if len(keys) == 1:
+            return {keys[0]: data_list[0]}
+        return {key: data_list[i] for i, key in enumerate(keys)}
+
+

5. 结果展示

+

使用混合精度训练后,在测试集上评估与目标之间的 MSE、PSNR、SSIM,评估指标的值为:

+ + + + + + + + + + + + + + + +
MSEPSNRSSIM
4.21e-547.190.9974
+

一个流体超分样例的输入、模型预测结果、数据集介绍中开源代码包 mantaflow 直接生成的结果如下,模型预测结果与生成的目标结果基本一致。

+
+

input +

+
输入的低密度流体
+
+
+

pred-amp02 +

+
混合精度训练后推理得到的高密度流体
+
+
+

target +

+
目标高密度流体
+
+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/topopt/index.html b/zh/examples/topopt/index.html new file mode 100644 index 0000000000..917ef52a11 --- /dev/null +++ b/zh/examples/topopt/index.html @@ -0,0 +1,6141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TopOpt - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

TopOpt

+

AI Studio快速体验

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py mode=eval 'EVAL.pretrained_model_path_dict={'Uniform': 'https://paddle-org.bj.bcebos.com/paddlescience/models/topopt/uniform_pretrained.pdparams', 'Poisson5': 'https://paddle-org.bj.bcebos.com/paddlescience/models/topopt/poisson5_pretrained.pdparams', 'Poisson10': 'https://paddle-org.bj.bcebos.com/paddlescience/models/topopt/poisson10_pretrained.pdparams', 'Poisson30': 'https://paddle-org.bj.bcebos.com/paddlescience/models/topopt/poisson30_pretrained.pdparams'}'
+
+
+
+
python topopt.py mode=export INFER.pretrained_model_name=Uniform
+
+
python topopt.py mode=export INFER.pretrained_model_name=Poisson5
+
+
python topopt.py mode=export INFER.pretrained_model_name=Poisson10
+
+
python topopt.py mode=export INFER.pretrained_model_name=Poisson30
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py mode=infer INFER.pretrained_model_name=Uniform INFER.img_num=3
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py mode=infer INFER.pretrained_model_name=Poisson5 INFER.img_num=3
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py mode=infer INFER.pretrained_model_name=Poisson10 INFER.img_num=3
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 -P ./datasets/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/topopt/top_dataset.h5 --create-dirs -o ./datasets/top_dataset.h5
+python topopt.py mode=infer INFER.pretrained_model_name=Poisson30 INFER.img_num=3
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
预训练模型指标
topopt_uniform_pretrained.pdparamsloss(sup_validator): [0.14336, 0.10211, 0.07927, 0.06433, 0.04970, 0.04612, 0.04201, 0.03566, 0.03623, 0.03314, 0.02929, 0.02857, 0.02498, 0.02517, 0.02523, 0.02618]
metric.Binary_Acc(sup_validator): [0.9410, 0.9673, 0.9718, 0.9727, 0.9818, 0.9824, 0.9826, 0.9845, 0.9856, 0.9892, 0.9892, 0.9907, 0.9890, 0.9916, 0.9914, 0.9922]
metric.IoU(sup_validator): [0.8887, 0.9367, 0.9452, 0.9468, 0.9644, 0.9655, 0.9659, 0.9695, 0.9717, 0.9787, 0.9787, 0.9816, 0.9784, 0.9835, 0.9831, 0.9845]
topopt_poisson5_pretrained.pdparamsloss(sup_validator): [0.11926, 0.09162, 0.08014, 0.06390, 0.05839, 0.05264, 0.04921, 0.04737, 0.04872, 0.04564, 0.04226, 0.04267, 0.04407, 0.04172, 0.03939, 0.03927]
metric.Binary_Acc(sup_validator): [0.9471, 0.9619, 0.9702, 0.9742, 0.9782, 0.9801, 0.9803, 0.9825, 0.9824, 0.9837, 0.9850, 0.9850, 0.9870, 0.9863, 0.9870, 0.9872]
metric.IoU(sup_validator): [0.8995, 0.9267, 0.9421, 0.9497, 0.9574, 0.9610, 0.9614, 0.9657, 0.9655, 0.9679, 0.9704, 0.9704, 0.9743, 0.9730, 0.9744, 0.9747]
topopt_poisson10_pretrained.pdparamsloss(sup_validator): [0.12886, 0.07201, 0.05946, 0.04622, 0.05072, 0.04178, 0.03823, 0.03677, 0.03623, 0.03029, 0.03398, 0.02978, 0.02861, 0.02946, 0.02831, 0.02817]
metric.Binary_Acc(sup_validator): [0.9457, 0.9703, 0.9745, 0.9798, 0.9827, 0.9845, 0.9859, 0.9870, 0.9882, 0.9880, 0.9893, 0.9899, 0.9882, 0.9899, 0.9905, 0.9904]
metric.IoU(sup_validator): [0.8969, 0.9424, 0.9502, 0.9604, 0.9660, 0.9696, 0.9722, 0.9743, 0.9767, 0.9762, 0.9789, 0.9800, 0.9768, 0.9801, 0.9813, 0.9810]
topopt_poisson30_pretrained.pdparamsloss(sup_validator): [0.19111, 0.10081, 0.06930, 0.04631, 0.03821, 0.03441, 0.02738, 0.03040, 0.02787, 0.02385, 0.02037, 0.02065, 0.01840, 0.01896, 0.01970, 0.01676]
metric.Binary_Acc(sup_validator): [0.9257, 0.9595, 0.9737, 0.9832, 0.9828, 0.9883, 0.9885, 0.9892, 0.9901, 0.9916, 0.9924, 0.9925, 0.9926, 0.9929, 0.9937, 0.9936]
metric.IoU(sup_validator): [0.8617, 0.9221, 0.9488, 0.9670, 0.9662, 0.9769, 0.9773, 0.9786, 0.9803, 0.9833, 0.9850, 0.9853, 0.9855, 0.9860, 0.9875, 0.9873]
+

1. 背景简介

+

拓扑优化 (Topolgy Optimization) 是一种数学方法,针对给定的一组负载、边界条件和约束,在给定的设计区域内,以最大化系统性能为目标优化材料的分布。这个问题很有挑战性因为它要求解决方案是二元的,即应该说明设计区域的每个部分是否存在材料或不存在。这种优化的一个常见例子是在给定总重量和边界条件下最小化物体的弹性应变能。随着20世纪汽车和航空航天工业的发展,拓扑优化已经将应用扩展到很多其他学科:如流体、声学、电磁学、光学及其组合。SIMP (Simplied Isotropic Material with Penalization) 是目前广泛传播的一种简单而高效的拓扑优化求解方法。它通过对材料密度的中间值进行惩罚,提高了二元解的收敛性。

+

2. 问题定义

+

拓扑优化问题:

+
\[ +\begin{aligned} +& \underset{\mathbf{x}}{\text{min}} \quad && c(\mathbf{u}(\mathbf{x}), \mathbf{x}) = \sum_{j=1}^{N} E_{j}(x_{j})\mathbf{u}_{j}^{\intercal}\mathbf{k}_{0}\mathbf{u}_{j} \\ +& \text{s.t.} \quad && V(\mathbf{x})/V_{0} = f_{0} \\ +& \quad && \mathbf{K}\mathbf{U} = \mathbf{F} \\ +& \quad && x_{j} \in \{0, 1\}, \quad j = 1,...,N +\end{aligned} +\]
+

其中:\(x_{j}\) 是材料分布 (material distribution);\(c\) 指可塑性 (compliance);\(\mathbf{u}_{j}\) 是 element displacement vector;\(\mathbf{k}_{0}\) 是 element stiffness matrix for an element with unit Youngs modulu;\(\mathbf{U}\), \(\mathbf{F}\) 是 global displacement and force vectors;\(\mathbf{K}\) 是 global stiffness matrix;\(V(\mathbf{x})\), \(V_{0}\) 是材料体积和设计区域的体积;\(f_{0}\) 是预先指定的体积比。

+

3. 问题求解

+

实际求解上述问题时为做简化,会把最后一个约束条件换成连续的形式:\(x_{j} \in [0, 1], \quad j = 1,...,N\)。 常见的优化算法是 SIMP 算法,它是一种基于梯度的迭代法,并对非二元解做惩罚:\(E_{j}(x_{j}) = E_{\text{min}} + x_{j}^{p}(E_{0} - E_{\text{min}})\),这里我们不对 SIMP 算法做过多展开。由于利用 SIMP 方法, 求解器只需要进行初始的 \(N_{0}\) 次迭代就可以得到与结果的最终结果非常相近的基本视图,本案例希望通过将 SIMP 的第 \(N_{0}\) 次初始迭代结果与其对应的梯度信息作为 Unet 的输入,预测 SIMP 的100次迭代步骤后给出的优化解。

+

3.1 数据集准备

+

下载的数据集为整理过的合成数据,整理后的格式为 "iters": shape = (10000, 100, 40, 40)"target": shape = (10000, 1, 40, 40)

+
    +
  • +

    10000 - 随机生成问题的个数

    +
  • +
  • +

    100 - SIMP 迭代次数

    +
  • +
  • +

    40 - 图像高度

    +
  • +
  • +

    40 - 图像宽度

    +
  • +
+

数据集地址请存储于 ./datasets/top_dataset.h5

+

生成训练集:原始代码利用所有的10000问题生成训练数据。

+
68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
def generate_train_test(
+    data_iters: np.ndarray,
+    data_targets: np.ndarray,
+    train_test_ratio: float,
+    n_sample: int,
+) -> Union[
+    Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
+]:
+    """Generate training and testing set
+
+    Args:
+        data_iters (np.ndarray): data with 100 channels corresponding to the results of 100 steps of SIMP algorithm
+        data_targets (np.ndarray): final optimization solution given by SIMP algorithm
+        train_test_ratio (float): split ratio of training and testing sets, if `train_test_ratio` = 1 then only return training data
+        n_sample (int): number of total samples in training and testing sets to be sampled from the h5 dataset
+
+    Returns:
+        Union[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: if `train_test_ratio` = 1, return (train_inputs, train_labels), else return (train_inputs, train_labels, test_inputs, test_labels)
+    """
+    n_obj = len(data_iters)
+    idx = np.arange(n_obj)
+    np.random.shuffle(idx)
+    train_idx = idx[: int(train_test_ratio * n_sample)]
+    if train_test_ratio == 1.0:
+        return data_iters[train_idx], data_targets[train_idx]
+
+    test_idx = idx[int(train_test_ratio * n_sample) :]
+    train_iters = data_iters[train_idx]
+    train_targets = data_targets[train_idx]
+    test_iters = data_iters[test_idx]
+    test_targets = data_targets[test_idx]
+    return train_iters, train_targets, test_iters, test_targets
+
+
40
+41
+42
+43
+44
+45
+46
+47
# read h5 data
+h5data = h5py.File(cfg.DATA_PATH, "r")
+data_iters = np.array(h5data["iters"])
+data_targets = np.array(h5data["targets"])
+
+# generate training dataset
+inputs_train, labels_train = func_module.generate_train_test(
+    data_iters, data_targets, cfg.train_test_ratio, cfg.n_samples
+
+

3.2 模型构建

+

经过 SIMP 的 \(N_{0}\) 次初始迭代步骤得到的图像 \(I\) 可以看作是模糊了的最终结构。由于最终的优化解给出的图像 \(I^*\) 并不包含中间过程的信息,因此 \(I^*\) 可以被解释为图像 \(I\) 的掩码。于是 \(I \rightarrow I^*\) 这一优化过程可以看作是二分类的图像分割或者前景-背景分割过程,因此构建 Unet 模型进行预测,具体网络结构如图所示: +Unet

+
# set model
+
+

详细的模型代码在 examples/topopt/topoptmodel.py 中。

+

3.3 参数设定

+

根据论文以及原始代码给出以下训练参数:

+
49
+50
+51
+52
+53
+54
# other parameters
+n_samples: 10000
+train_test_ratio: 1.0 # use 10000 original data with different channels for training
+vol_coeff: 1 # coefficient for volume fraction constraint in the loss - beta in equation (3) in paper
+
+# training settings
+
+
# 4 training cases parameters
+LEARNING_RATE = cfg.TRAIN.learning_rate / (1 + cfg.TRAIN.epochs // 15)
+
+

3.4 data transform

+

根据论文以及原始代码给出以下自定义的 data transform 代码,包括随机水平或垂直翻转和随机90度旋转,对 input 和 label 同时 transform:

+
def augmentation(
+    input_dict: Dict[str, np.ndarray],
+    label_dict: Dict[str, np.ndarray],
+    weight_dict: Dict[str, np.ndarray] = None,
+) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray]]:
+    """Apply random transformation from D4 symmetry group
+
+    Args:
+        input_dict (Dict[str, np.ndarray]): input dict of np.ndarray size `(batch_size, any, height, width)`
+        label_dict (Dict[str, np.ndarray]): label dict of np.ndarray size `(batch_size, 1, height, width)`
+        weight_dict (Dict[str, np.ndarray]): weight dict if any
+    """
+    inputs = input_dict["input"]
+    labels = label_dict["output"]
+    assert len(inputs.shape) == 3
+    assert len(labels.shape) == 3
+
+    # random horizontal flip
+    if np.random.random() > 0.5:
+        inputs = np.flip(inputs, axis=2)
+        labels = np.flip(labels, axis=2)
+    # random vertical flip
+    if np.random.random() > 0.5:
+        inputs = np.flip(inputs, axis=1)
+        labels = np.flip(labels, axis=1)
+    # random 90* rotation
+    if np.random.random() > 0.5:
+        new_perm = list(range(len(inputs.shape)))
+        new_perm[-2], new_perm[-1] = new_perm[-1], new_perm[-2]
+        inputs = np.transpose(inputs, new_perm)
+        labels = np.transpose(labels, new_perm)
+
+

3.5 约束构建

+

在本案例中,我们采用监督学习方式进行训练,所以使用监督约束 SupervisedConstraint,代码如下:

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
# set constraints
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"input": inputs_train},
+            "label": {"output": labels_train},
+            "transforms": (
+                {
+                    "FunctionalTransform": {
+                        "transform_func": func_module.augmentation,
+                    },
+                },
+            ),
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.FunctionalLoss(loss_wrapper(cfg)),
+    name="sup_constraint",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,配置中 "dataset" 字段表示使用的训练数据集信息,其各个字段分别表示:

+
    +
  1. name: 数据集类型,此处 "NamedArrayDataset" 表示分 batch 顺序读取的 np.ndarray 类型的数据集;
  2. +
  3. input: 输入变量字典:{"input_name": input_dataset}
  4. +
  5. label: 标签变量字典:{"label_name": label_dataset}
  6. +
  7. transforms: 数据集预处理配,其中 "FunctionalTransform" 为用户自定义的预处理方式。
  8. +
+

读取配置中 "batch_size" 字段表示训练时指定的批大小,"sampler" 字段表示 dataloader 的相关采样配置。

+

第二个参数是损失函数,这里使用自定义损失,通过 cfg.vol_coeff 确定损失公式中 \(\beta\) 对应的值。

+

第三个参数是约束条件的名字,方便后续对其索引。此次命名为 "sup_constraint"

+

在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+

3.6 采样器构建

+

原始数据第二维有100个通道,对应的是 SIMP 算法 100 次的迭代结果,本案例模型目标是用 SIMP 中间某一步的迭代结果直接预测 SIMP 算法100步迭代后最终的优化求解结果,这里需要构建一个通道采样器,用来将输入模型数据的第二维按一定的概率分布随机抽取某一通道或直接指定某一通道,再输入网络进行训练或推理。本案例将采样步骤放入模型的 forward 方法中。

+
23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
def uniform_sampler() -> Callable[[], int]:
+    """Generate uniform sampling function from 1 to 99
+
+    Returns:
+        sampler (Callable[[], int]): uniform sampling from 1 to 99
+    """
+    return lambda: np.random.randint(1, 99)
+
+
+def poisson_sampler(lam: int) -> Callable[[], int]:
+    """Generate poisson sampling function with parameter lam with range 1 to 99
+
+    Args:
+        lam (int): poisson rate parameter
+
+    Returns:
+        sampler (Callable[[], int]): poisson sampling function with parameter lam with range 1 to 99
+    """
+
+    def func():
+        iter_ = max(np.random.poisson(lam), 1)
+        iter_ = min(iter_, 99)
+        return iter_
+
+    return func
+
+
+def generate_sampler(sampler_type: str = "Fixed", num: int = 0) -> Callable[[], int]:
+    """Generate sampler for the number of initial iteration steps
+
+    Args:
+        sampler_type (str): "Poisson" for poisson sampler; "Uniform" for uniform sampler; "Fixed" for choosing a fixed number of initial iteration steps.
+        num (int): If `sampler_type` == "Poisson", `num` specifies the poisson rate parameter; If `sampler_type` == "Fixed", `num` specifies the fixed number of initial iteration steps.
+
+    Returns:
+        sampler (Callable[[], int]): sampler for the number of initial iteration steps
+    """
+    if sampler_type == "Poisson":
+        return poisson_sampler(num)
+    elif sampler_type == "Uniform":
+        return uniform_sampler()
+    else:
+        return lambda: num
+
+
# initialize SIMP iteration stop time sampler
+
+

3.7 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择 Adam 优化器。

+
93
+94
+95
# set optimizer
+optimizer = ppsci.optimizer.Adam(learning_rate=LEARNING_RATE, epsilon=1.0e-7)(
+    model
+
+

3.8 loss和metric构建

+

3.8.1 loss构建

+

损失函数为 confidence loss + beta * volume fraction constraints:

+
\[ +\mathcal{L} = \mathcal{L}_{\text{conf}}(X_{\text{true}}, X_{\text{pred}}) + \beta * \mathcal{L}_{\text{vol}}(X_{\text{true}}, X_{\text{pred}}) +\]
+

confidence loss 是 binary cross-entropy:

+
\[ +\mathcal{L}_{\text{conf}}(X_{\text{true}}, X_{\text{pred}}) = -\frac{1}{NM}\sum_{i=1}^{N}\sum_{j=1}^{M}\left[X_{\text{true}}^{ij}\log(X_{\text{pred}}^{ij}) + (1 - X_{\text{true}}^{ij})\log(1 - X_{\text{pred}}^{ij})\right] +\]
+

volume fraction constraints:

+
\[ +\mathcal{L}_{\text{vol}}(X_{\text{true}}, X_{\text{pred}}) = (\bar{X}_{\text{pred}} - \bar{X}_{\text{true}})^2 +\]
+

loss 构建代码如下:

+
# define loss wrapper
+def loss_wrapper(cfg: DictConfig):
+    def loss_expr(output_dict, label_dict, weight_dict=None):
+        label_true = label_dict["output"].reshape((-1, 1))
+        label_pred = output_dict["output"].reshape((-1, 1))
+        conf_loss = paddle.mean(
+            nn.functional.log_loss(label_pred, label_true, epsilon=1.0e-7)
+        )
+        vol_loss = paddle.square(paddle.mean(label_true - label_pred))
+        return {"output": conf_loss + cfg.vol_coeff * vol_loss}
+
+

3.8.2 metric构建

+

本案例原始代码选择 Binary Accuracy 和 IoU 进行评估:

+
\[ +\text{Bin. Acc.} = \frac{w_{00}+w_{11}}{n_{0}+n_{1}} +\]
+
\[ +\text{IoU} = \frac{1}{2}\left[\frac{w_{00}}{n_{0}+w_{10}} + \frac{w_{11}}{n_{1}+w_{01}}\right] +\]
+

其中 \(n_{0} = w_{00} + w_{01}\)\(n_{1} = w_{10} + w_{11}\)\(w_{tp}\) 表示实际是 \(t\) 类且被预测为 \(p\) 类的像素点的数量 +metric 构建代码如下:

+
# define metric
+def val_metric(output_dict, label_dict, weight_dict=None):
+    label_pred = output_dict["output"]
+    label_true = label_dict["output"]
+    accurates = paddle.equal(paddle.round(label_true), paddle.round(label_pred))
+    acc = paddle.mean(paddle.cast(accurates, dtype=paddle.get_default_dtype()))
+    true_negative = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 0.0),
+            paddle.equal(paddle.round(label_true), 0.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    true_positive = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 1.0),
+            paddle.equal(paddle.round(label_true), 1.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    false_negative = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 1.0),
+            paddle.equal(paddle.round(label_true), 0.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    false_positive = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 0.0),
+            paddle.equal(paddle.round(label_true), 1.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    n_negative = paddle.add(false_negative, true_negative)
+    n_positive = paddle.add(true_positive, false_positive)
+    iou = 0.5 * paddle.add(
+        paddle.divide(true_negative, paddle.add(n_negative, false_positive)),
+        paddle.divide(true_positive, paddle.add(n_positive, false_negative)),
+    )
+
+

3.9 模型训练

+

本案例根据采样器的不同选择共有四组子案例,案例参数如下:

+
29
+30
+31
# general settings
+mode: train # running mode: train/eval
+seed: 42
+
+

训练代码如下:

+
# train models for 4 cases
+for sampler_key, num in cfg.CASE_PARAM:
+
+    # initialize SIMP iteration stop time sampler
+    SIMP_stop_point_sampler = func_module.generate_sampler(sampler_key, num)
+
+    # initialize logger for training
+    sampler_name = sampler_key + str(num) if num else sampler_key
+    OUTPUT_DIR = osp.join(
+        cfg.output_dir, f"{sampler_name}_vol_coeff{cfg.vol_coeff}"
+    )
+    logger.init_logger("ppsci", osp.join(OUTPUT_DIR, "train.log"), "info")
+
+    # set model
+    model = TopOptNN(**cfg.MODEL, channel_sampler=SIMP_stop_point_sampler)
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(learning_rate=LEARNING_RATE, epsilon=1.0e-7)(
+        model
+    )
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        OUTPUT_DIR,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=ITERS_PER_EPOCH,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        seed=cfg.seed,
+    )
+
+    # train model
+
+

3.10 评估模型

+

对四个训练好的模型,分别使用不同的通道采样器 (原始数据的第二维对应表示的是 SIMP 算法的 100 步输出结果,统一取原始数据第二维的第 5,10,15,20,...,80 通道以及对应的梯度信息作为新的输入构建评估数据集) 进行评估,每次评估时只取 cfg.EVAL.num_val_step 个 bacth 的数据,计算它们的平均 Binary Accuracy 和 IoU 指标;同时评估结果需要与输入数据本身的阈值判定结果 (0.5作为阈值) 作比较。具体代码请参考完整代码

+

3.10.1 评估器构建

+

为应用 PaddleScience API,此处在每一次评估时构建一个评估器 SupervisedValidator 进行评估:

+
sup_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "NamedArrayDataset",
+            "input": {"input": inputs_eval},
+            "label": {"output": labels_eval},
+            "transforms": (
+                {
+                    "FunctionalTransform": {
+                        "transform_func": func_module.augmentation,
+                    },
+                },
+            ),
+        },
+        "batch_size": cfg.EVAL.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+        "num_workers": 0,
+    },
+    ppsci.loss.FunctionalLoss(loss_wrapper(cfg)),
+    {"output": lambda out: out["output"]},
+    {"metric": ppsci.metric.FunctionalMetric(val_metric)},
+    name="sup_validator",
+)
+
+

评估器配置与 约束构建 的设置类似,读取配置中 "num_workers":0 表示单线程读取;评价指标 "metric" 为自定义评估指标,包含 Binary Accuracy 和 IoU。

+

3.11 评估结果可视化

+

使用 ppsci.utils.misc.plot_curve() 方法直接绘制 Binary Accuracy 和 IoU 的结果:

+
ppsci.utils.misc.plot_curve(
+    acc_results_summary,
+    xlabel="iteration",
+    ylabel="accuracy",
+    output_dir=cfg.output_dir,
+)
+ppsci.utils.misc.plot_curve(
+    iou_results_summary, xlabel="iteration", ylabel="iou", output_dir=cfg.output_dir
+
+

4. 完整代码

+
topopt.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import path as osp
+from typing import Dict
+
+import functions as func_module
+import h5py
+import hydra
+import numpy as np
+import paddle
+from omegaconf import DictConfig
+from paddle import nn
+from topoptmodel import TopOptNN
+
+import ppsci
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # 4 training cases parameters
+    LEARNING_RATE = cfg.TRAIN.learning_rate / (1 + cfg.TRAIN.epochs // 15)
+    ITERS_PER_EPOCH = int(cfg.n_samples * cfg.train_test_ratio / cfg.TRAIN.batch_size)
+
+    # read h5 data
+    h5data = h5py.File(cfg.DATA_PATH, "r")
+    data_iters = np.array(h5data["iters"])
+    data_targets = np.array(h5data["targets"])
+
+    # generate training dataset
+    inputs_train, labels_train = func_module.generate_train_test(
+        data_iters, data_targets, cfg.train_test_ratio, cfg.n_samples
+    )
+
+    # set constraints
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "NamedArrayDataset",
+                "input": {"input": inputs_train},
+                "label": {"output": labels_train},
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": func_module.augmentation,
+                        },
+                    },
+                ),
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.FunctionalLoss(loss_wrapper(cfg)),
+        name="sup_constraint",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # train models for 4 cases
+    for sampler_key, num in cfg.CASE_PARAM:
+
+        # initialize SIMP iteration stop time sampler
+        SIMP_stop_point_sampler = func_module.generate_sampler(sampler_key, num)
+
+        # initialize logger for training
+        sampler_name = sampler_key + str(num) if num else sampler_key
+        OUTPUT_DIR = osp.join(
+            cfg.output_dir, f"{sampler_name}_vol_coeff{cfg.vol_coeff}"
+        )
+        logger.init_logger("ppsci", osp.join(OUTPUT_DIR, "train.log"), "info")
+
+        # set model
+        model = TopOptNN(**cfg.MODEL, channel_sampler=SIMP_stop_point_sampler)
+
+        # set optimizer
+        optimizer = ppsci.optimizer.Adam(learning_rate=LEARNING_RATE, epsilon=1.0e-7)(
+            model
+        )
+
+        # initialize solver
+        solver = ppsci.solver.Solver(
+            model,
+            constraint,
+            OUTPUT_DIR,
+            optimizer,
+            epochs=cfg.TRAIN.epochs,
+            iters_per_epoch=ITERS_PER_EPOCH,
+            eval_during_train=cfg.TRAIN.eval_during_train,
+            seed=cfg.seed,
+        )
+
+        # train model
+        solver.train()
+
+
+# evaluate 4 models
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+    # initialize logger
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info")
+
+    # fixed iteration stop times for evaluation
+    iterations_stop_times = range(5, 85, 5)
+    model = TopOptNN(**cfg.MODEL)
+
+    # evaluation for 4 cases
+    acc_results_summary = {}
+    iou_results_summary = {}
+
+    # read h5 data
+    h5data = h5py.File(cfg.DATA_PATH, "r")
+    data_iters = np.array(h5data["iters"])
+    data_targets = np.array(h5data["targets"])
+
+    for case_name, model_path in cfg.EVAL.pretrained_model_path_dict.items():
+        acc_results, iou_results = evaluate_model(
+            cfg, model, model_path, data_iters, data_targets, iterations_stop_times
+        )
+
+        acc_results_summary[case_name] = acc_results
+        iou_results_summary[case_name] = iou_results
+
+    # calculate thresholding results
+    th_acc_results = []
+    th_iou_results = []
+    for stop_iter in iterations_stop_times:
+        SIMP_stop_point_sampler = func_module.generate_sampler("Fixed", stop_iter)
+
+        current_acc_results = []
+        current_iou_results = []
+
+        # only calculate for NUM_VAL_STEP times of iteration
+        for _ in range(cfg.EVAL.num_val_step):
+            input_full_channel, label = func_module.generate_train_test(
+                data_iters, data_targets, 1.0, cfg.EVAL.batch_size
+            )
+            # thresholding
+            SIMP_initial_iter_time = SIMP_stop_point_sampler()  # channel k
+            input_channel_k = paddle.to_tensor(
+                input_full_channel, dtype=paddle.get_default_dtype()
+            )[:, SIMP_initial_iter_time, :, :]
+            input_channel_k_minus_1 = paddle.to_tensor(
+                input_full_channel, dtype=paddle.get_default_dtype()
+            )[:, SIMP_initial_iter_time - 1, :, :]
+            input = paddle.stack(
+                (input_channel_k, input_channel_k - input_channel_k_minus_1), axis=1
+            )
+            out = paddle.cast(
+                paddle.to_tensor(input)[:, 0:1, :, :] > 0.5,
+                dtype=paddle.get_default_dtype(),
+            )
+            th_result = val_metric(
+                {"output": out},
+                {"output": paddle.to_tensor(label, dtype=paddle.get_default_dtype())},
+            )
+            acc_results, iou_results = th_result["Binary_Acc"], th_result["IoU"]
+            current_acc_results.append(acc_results)
+            current_iou_results.append(iou_results)
+
+        th_acc_results.append(np.mean(current_acc_results))
+        th_iou_results.append(np.mean(current_iou_results))
+
+    acc_results_summary["thresholding"] = th_acc_results
+    iou_results_summary["thresholding"] = th_iou_results
+
+    ppsci.utils.misc.plot_curve(
+        acc_results_summary,
+        xlabel="iteration",
+        ylabel="accuracy",
+        output_dir=cfg.output_dir,
+    )
+    ppsci.utils.misc.plot_curve(
+        iou_results_summary, xlabel="iteration", ylabel="iou", output_dir=cfg.output_dir
+    )
+
+
+def evaluate_model(
+    cfg, model, pretrained_model_path, data_iters, data_targets, iterations_stop_times
+):
+    # load model parameters
+    solver = ppsci.solver.Solver(
+        model,
+        epochs=1,
+        iters_per_epoch=cfg.EVAL.num_val_step,
+        eval_with_no_grad=True,
+        pretrained_model_path=pretrained_model_path,
+    )
+
+    acc_results = []
+    iou_results = []
+
+    # evaluation for different fixed iteration stop times
+    for stop_iter in iterations_stop_times:
+        # only evaluate for NUM_VAL_STEP times of iteration
+        inputs_eval, labels_eval = func_module.generate_train_test(
+            data_iters, data_targets, 1.0, cfg.EVAL.batch_size * cfg.EVAL.num_val_step
+        )
+
+        sup_validator = ppsci.validate.SupervisedValidator(
+            {
+                "dataset": {
+                    "name": "NamedArrayDataset",
+                    "input": {"input": inputs_eval},
+                    "label": {"output": labels_eval},
+                    "transforms": (
+                        {
+                            "FunctionalTransform": {
+                                "transform_func": func_module.augmentation,
+                            },
+                        },
+                    ),
+                },
+                "batch_size": cfg.EVAL.batch_size,
+                "sampler": {
+                    "name": "BatchSampler",
+                    "drop_last": False,
+                    "shuffle": True,
+                },
+                "num_workers": 0,
+            },
+            ppsci.loss.FunctionalLoss(loss_wrapper(cfg)),
+            {"output": lambda out: out["output"]},
+            {"metric": ppsci.metric.FunctionalMetric(val_metric)},
+            name="sup_validator",
+        )
+        validator = {sup_validator.name: sup_validator}
+        solver.validator = validator
+
+        # modify the channel_sampler in model
+        SIMP_stop_point_sampler = func_module.generate_sampler("Fixed", stop_iter)
+        solver.model.channel_sampler = SIMP_stop_point_sampler
+
+        _, eval_result = solver.eval()
+
+        current_acc_results = eval_result["metric"]["Binary_Acc"]
+        current_iou_results = eval_result["metric"]["IoU"]
+
+        acc_results.append(current_acc_results)
+        iou_results.append(current_iou_results)
+
+    return acc_results, iou_results
+
+
+# define loss wrapper
+def loss_wrapper(cfg: DictConfig):
+    def loss_expr(output_dict, label_dict, weight_dict=None):
+        label_true = label_dict["output"].reshape((-1, 1))
+        label_pred = output_dict["output"].reshape((-1, 1))
+        conf_loss = paddle.mean(
+            nn.functional.log_loss(label_pred, label_true, epsilon=1.0e-7)
+        )
+        vol_loss = paddle.square(paddle.mean(label_true - label_pred))
+        return {"output": conf_loss + cfg.vol_coeff * vol_loss}
+
+    return loss_expr
+
+
+# define metric
+def val_metric(output_dict, label_dict, weight_dict=None):
+    label_pred = output_dict["output"]
+    label_true = label_dict["output"]
+    accurates = paddle.equal(paddle.round(label_true), paddle.round(label_pred))
+    acc = paddle.mean(paddle.cast(accurates, dtype=paddle.get_default_dtype()))
+    true_negative = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 0.0),
+            paddle.equal(paddle.round(label_true), 0.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    true_positive = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 1.0),
+            paddle.equal(paddle.round(label_true), 1.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    false_negative = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 1.0),
+            paddle.equal(paddle.round(label_true), 0.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    false_positive = paddle.sum(
+        paddle.multiply(
+            paddle.equal(paddle.round(label_pred), 0.0),
+            paddle.equal(paddle.round(label_true), 1.0),
+        ),
+        dtype=paddle.get_default_dtype(),
+    )
+    n_negative = paddle.add(false_negative, true_negative)
+    n_positive = paddle.add(true_positive, false_positive)
+    iou = 0.5 * paddle.add(
+        paddle.divide(true_negative, paddle.add(n_negative, false_positive)),
+        paddle.divide(true_positive, paddle.add(n_positive, false_negative)),
+    )
+    return {"Binary_Acc": acc, "IoU": iou}
+
+
+# export model
+def export(cfg: DictConfig):
+    # set model
+    model = TopOptNN(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        eval_with_no_grad=True,
+        pretrained_model_path=cfg.INFER.pretrained_model_path_dict[
+            cfg.INFER.pretrained_model_name
+        ],
+    )
+
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [{"input": InputSpec([None, 2, 40, 40], "float32", name="input")}]
+
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    # read h5 data
+    h5data = h5py.File(cfg.DATA_PATH, "r")
+    data_iters = np.array(h5data["iters"])
+    data_targets = np.array(h5data["targets"])
+    idx = np.random.choice(len(data_iters), cfg.INFER.img_num, False)
+    data_iters = data_iters[idx]
+    data_targets = data_targets[idx]
+
+    sampler = func_module.generate_sampler(cfg.INFER.sampler_key, cfg.INFER.sampler_num)
+    data_iters = channel_sampling(sampler, data_iters)
+
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    input_dict = {"input": data_iters}
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to output_key
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip({"output"}, output_dict.keys())
+    }
+
+    save_topopt_img(
+        input_dict,
+        output_dict,
+        data_targets,
+        cfg.INFER.save_res_path,
+        cfg.INFER.res_img_figsize,
+        cfg.INFER.save_npy,
+    )
+
+
+# used for inference
+def channel_sampling(sampler, input):
+    SIMP_initial_iter_time = sampler()
+    input_channel_k = input[:, SIMP_initial_iter_time, :, :]
+    input_channel_k_minus_1 = input[:, SIMP_initial_iter_time - 1, :, :]
+    input = np.stack(
+        (input_channel_k, input_channel_k - input_channel_k_minus_1), axis=1
+    )
+    return input
+
+
+# used for inference
+def save_topopt_img(
+    input_dict: Dict[str, np.ndarray],
+    output_dict: Dict[str, np.ndarray],
+    ground_truth: np.ndarray,
+    save_dir: str,
+    figsize: tuple = None,
+    save_npy: bool = False,
+):
+
+    input = input_dict["input"]
+    output = output_dict["output"]
+    import os
+
+    import matplotlib.pyplot as plt
+
+    os.makedirs(save_dir, exist_ok=True)
+    for i in range(len(input)):
+        plt.figure(figsize=figsize)
+        plt.subplot(1, 4, 1)
+        plt.axis("off")
+        plt.imshow(input[i][0], cmap="gray")
+        plt.title("Input Image")
+        plt.subplot(1, 4, 2)
+        plt.axis("off")
+        plt.imshow(input[i][1], cmap="gray")
+        plt.title("Input Gradient")
+        plt.subplot(1, 4, 3)
+        plt.axis("off")
+        plt.imshow(np.round(output[i][0]), cmap="gray")
+        plt.title("Prediction")
+        plt.subplot(1, 4, 4)
+        plt.axis("off")
+        plt.imshow(np.round(ground_truth[i][0]), cmap="gray")
+        plt.title("Ground Truth")
+        plt.show()
+        plt.savefig(osp.join(save_dir, f"Prediction_{i}.png"))
+        plt.close()
+        if save_npy:
+            with open(osp(save_dir, f"Prediction_{i}.npy"), "wb") as f:
+                np.save(f, output[i])
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="topopt.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+
functions.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Callable
+from typing import Dict
+from typing import Tuple
+from typing import Union
+
+import numpy as np
+
+
+def uniform_sampler() -> Callable[[], int]:
+    """Generate uniform sampling function from 1 to 99
+
+    Returns:
+        sampler (Callable[[], int]): uniform sampling from 1 to 99
+    """
+    return lambda: np.random.randint(1, 99)
+
+
+def poisson_sampler(lam: int) -> Callable[[], int]:
+    """Generate poisson sampling function with parameter lam with range 1 to 99
+
+    Args:
+        lam (int): poisson rate parameter
+
+    Returns:
+        sampler (Callable[[], int]): poisson sampling function with parameter lam with range 1 to 99
+    """
+
+    def func():
+        iter_ = max(np.random.poisson(lam), 1)
+        iter_ = min(iter_, 99)
+        return iter_
+
+    return func
+
+
+def generate_sampler(sampler_type: str = "Fixed", num: int = 0) -> Callable[[], int]:
+    """Generate sampler for the number of initial iteration steps
+
+    Args:
+        sampler_type (str): "Poisson" for poisson sampler; "Uniform" for uniform sampler; "Fixed" for choosing a fixed number of initial iteration steps.
+        num (int): If `sampler_type` == "Poisson", `num` specifies the poisson rate parameter; If `sampler_type` == "Fixed", `num` specifies the fixed number of initial iteration steps.
+
+    Returns:
+        sampler (Callable[[], int]): sampler for the number of initial iteration steps
+    """
+    if sampler_type == "Poisson":
+        return poisson_sampler(num)
+    elif sampler_type == "Uniform":
+        return uniform_sampler()
+    else:
+        return lambda: num
+
+
+def generate_train_test(
+    data_iters: np.ndarray,
+    data_targets: np.ndarray,
+    train_test_ratio: float,
+    n_sample: int,
+) -> Union[
+    Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
+]:
+    """Generate training and testing set
+
+    Args:
+        data_iters (np.ndarray): data with 100 channels corresponding to the results of 100 steps of SIMP algorithm
+        data_targets (np.ndarray): final optimization solution given by SIMP algorithm
+        train_test_ratio (float): split ratio of training and testing sets, if `train_test_ratio` = 1 then only return training data
+        n_sample (int): number of total samples in training and testing sets to be sampled from the h5 dataset
+
+    Returns:
+        Union[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]]: if `train_test_ratio` = 1, return (train_inputs, train_labels), else return (train_inputs, train_labels, test_inputs, test_labels)
+    """
+    n_obj = len(data_iters)
+    idx = np.arange(n_obj)
+    np.random.shuffle(idx)
+    train_idx = idx[: int(train_test_ratio * n_sample)]
+    if train_test_ratio == 1.0:
+        return data_iters[train_idx], data_targets[train_idx]
+
+    test_idx = idx[int(train_test_ratio * n_sample) :]
+    train_iters = data_iters[train_idx]
+    train_targets = data_targets[train_idx]
+    test_iters = data_iters[test_idx]
+    test_targets = data_targets[test_idx]
+    return train_iters, train_targets, test_iters, test_targets
+
+
+def augmentation(
+    input_dict: Dict[str, np.ndarray],
+    label_dict: Dict[str, np.ndarray],
+    weight_dict: Dict[str, np.ndarray] = None,
+) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], Dict[str, np.ndarray]]:
+    """Apply random transformation from D4 symmetry group
+
+    Args:
+        input_dict (Dict[str, np.ndarray]): input dict of np.ndarray size `(batch_size, any, height, width)`
+        label_dict (Dict[str, np.ndarray]): label dict of np.ndarray size `(batch_size, 1, height, width)`
+        weight_dict (Dict[str, np.ndarray]): weight dict if any
+    """
+    inputs = input_dict["input"]
+    labels = label_dict["output"]
+    assert len(inputs.shape) == 3
+    assert len(labels.shape) == 3
+
+    # random horizontal flip
+    if np.random.random() > 0.5:
+        inputs = np.flip(inputs, axis=2)
+        labels = np.flip(labels, axis=2)
+    # random vertical flip
+    if np.random.random() > 0.5:
+        inputs = np.flip(inputs, axis=1)
+        labels = np.flip(labels, axis=1)
+    # random 90* rotation
+    if np.random.random() > 0.5:
+        new_perm = list(range(len(inputs.shape)))
+        new_perm[-2], new_perm[-1] = new_perm[-1], new_perm[-2]
+        inputs = np.transpose(inputs, new_perm)
+        labels = np.transpose(labels, new_perm)
+
+    return {"input": inputs}, {"output": labels}, weight_dict
+
+
topoptmodel.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import paddle
+from paddle import nn
+
+import ppsci
+
+
+# NCHW data format
+class TopOptNN(ppsci.arch.UNetEx):
+    """Neural network for Topology Optimization, inherited from `ppsci.arch.UNetEx`
+
+    [Sosnovik, I., & Oseledets, I. (2019). Neural networks for topology optimization. Russian Journal of Numerical Analysis and Mathematical Modelling, 34(4), 215-223.](https://arxiv.org/pdf/1709.09578)
+
+    Args:
+        input_key (str): Name of function data for input.
+        output_key (str): Name of function data for output.
+        in_channel (int): Number of channels of input.
+        out_channel (int): Number of channels of output.
+        kernel_size (int, optional): Size of kernel of convolution layer. Defaults to 3.
+        filters (Tuple[int, ...], optional): Number of filters. Defaults to (16, 32, 64).
+        layers (int, optional): Number of encoders or decoders. Defaults to 3.
+        channel_sampler (callable, optional): The sampling function for the initial iteration time
+                (corresponding to the channel number of the input) of SIMP algorithm. The default value
+                is None, when it is None, input for the forward method should be sampled and prepared
+                with the shape of [batch, 2, height, width] before passing to forward method.
+        weight_norm (bool, optional): Whether use weight normalization layer. Defaults to True.
+        batch_norm (bool, optional): Whether add batch normalization layer. Defaults to True.
+        activation (Type[nn.Layer], optional): Name of activation function. Defaults to nn.ReLU.
+
+    Examples:
+        >>> import ppsci
+        >>> model = ppsci.arch.ppsci.arch.TopOptNN("input", "output", 2, 1, 3, (16, 32, 64), 2, lambda: 1, Flase, False)
+    """
+
+    def __init__(
+        self,
+        input_key="input",
+        output_key="output",
+        in_channel=2,
+        out_channel=1,
+        kernel_size=3,
+        filters=(16, 32, 64),
+        layers=2,
+        channel_sampler=None,
+        weight_norm=False,
+        batch_norm=False,
+        activation=nn.ReLU,
+    ):
+        super().__init__(
+            input_key=input_key,
+            output_key=output_key,
+            in_channel=in_channel,
+            out_channel=out_channel,
+            kernel_size=kernel_size,
+            filters=filters,
+            layers=layers,
+            weight_norm=weight_norm,
+            batch_norm=batch_norm,
+            activation=activation,
+        )
+        self.in_channel = in_channel
+        self.out_channel = out_channel
+        self.filters = filters
+        self.channel_sampler = channel_sampler
+        self.activation = activation
+
+        # Modify Layers
+        self.encoder[1] = nn.Sequential(
+            nn.MaxPool2D(self.in_channel, padding="SAME"),
+            self.encoder[1][0],
+            nn.Dropout2D(0.1),
+            self.encoder[1][1],
+        )
+        self.encoder[2] = nn.Sequential(
+            nn.MaxPool2D(2, padding="SAME"), self.encoder[2]
+        )
+        # Conv2D used in reference code in decoder
+        self.decoders[0] = nn.Sequential(
+            nn.Conv2D(
+                self.filters[-1], self.filters[-1], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+            nn.Conv2D(
+                self.filters[-1], self.filters[-1], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+        )
+        self.decoders[1] = nn.Sequential(
+            nn.Conv2D(
+                sum(self.filters[-2:]), self.filters[-2], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+            nn.Dropout2D(0.1),
+            nn.Conv2D(
+                self.filters[-2], self.filters[-2], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+        )
+        self.decoders[2] = nn.Sequential(
+            nn.Conv2D(
+                sum(self.filters[:-1]), self.filters[-3], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+            nn.Conv2D(
+                self.filters[-3], self.filters[-3], kernel_size=3, padding="SAME"
+            ),
+            self.activation(),
+        )
+        self.output = nn.Sequential(
+            nn.Conv2D(
+                self.filters[-3], self.out_channel, kernel_size=3, padding="SAME"
+            ),
+            nn.Sigmoid(),
+        )
+
+    def forward(self, x):
+        if self.channel_sampler is not None:
+            SIMP_initial_iter_time = self.channel_sampler()  # channel k
+            input_channel_k = x[self.input_keys[0]][:, SIMP_initial_iter_time, :, :]
+            input_channel_k_minus_1 = x[self.input_keys[0]][
+                :, SIMP_initial_iter_time - 1, :, :
+            ]
+            x = paddle.stack(
+                (input_channel_k, input_channel_k - input_channel_k_minus_1), axis=1
+            )
+        else:
+            x = x[self.input_keys[0]]
+        # encode
+        upsampling_size = []
+        skip_connection = []
+        n_encoder = len(self.encoder)
+        for i in range(n_encoder):
+            x = self.encoder[i](x)
+            if i is not (n_encoder - 1):
+                upsampling_size.append(x.shape[-2:])
+                skip_connection.append(x)
+
+        # decode
+        n_decoder = len(self.decoders)
+        for i in range(n_decoder):
+            x = self.decoders[i](x)
+            if i is not (n_decoder - 1):
+                up_size = upsampling_size.pop()
+                x = nn.UpsamplingNearest2D(up_size)(x)
+                skip_output = skip_connection.pop()
+                x = paddle.concat((skip_output, x), axis=1)
+
+        out = self.output(x)
+        return {self.output_keys[0]: out}
+
+

5. 结果展示

+

下图展示了4个模型分别在16组不同的评估数据集上的表现,包括 Binary Accuracy 以及 IoU 这两种指标。其中横坐标代表不同的评估数据集,例如:横坐标 \(i\) 表示由原始数据第二维的第 \(5\cdot(i+1)\) 个通道及其对应梯度信息构建的评估数据集;纵坐标为评估指标。thresholding 对应的指标可以理解为 benchmark。

+
+

bin_acc +

+
Binary Accuracy结果
+
+
+

iou +

+
IoU结果
+
+

用表格表示上图指标为:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
bin_acceval_dataset_ch_5eval_dataset_ch_10eval_dataset_ch_15eval_dataset_ch_20eval_dataset_ch_25eval_dataset_ch_30eval_dataset_ch_35eval_dataset_ch_40eval_dataset_ch_45eval_dataset_ch_50eval_dataset_ch_55eval_dataset_ch_60eval_dataset_ch_65eval_dataset_ch_70eval_dataset_ch_75eval_dataset_ch_80
Poisson50.94710.96190.97020.97420.97820.98010.98030.98250.98240.98370.98500.98500.98700.98630.98700.9872
Poisson100.94570.97030.97450.97980.98270.98450.98590.98700.98820.98800.98930.98990.98820.98990.99050.9904
Poisson300.92570.95950.97370.98320.98280.98830.98850.98920.99010.99160.99240.99250.99260.99290.99370.9936
Uniform0.94100.96730.97180.97270.98180.98240.98260.98450.98560.98920.98920.99070.98900.99160.99140.9922
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ioueval_dataset_ch_5eval_dataset_ch_10eval_dataset_ch_15eval_dataset_ch_20eval_dataset_ch_25eval_dataset_ch_30eval_dataset_ch_35eval_dataset_ch_40eval_dataset_ch_45eval_dataset_ch_50eval_dataset_ch_55eval_dataset_ch_60eval_dataset_ch_65eval_dataset_ch_70eval_dataset_ch_75eval_dataset_ch_80
Poisson50.89950.92670.94210.94970.95740.96100.96140.96570.96550.96790.97040.97040.97430.97300.97440.9747
Poisson100.89690.94240.95020.96040.96600.96960.97220.97430.97670.97620.97890.98000.97680.98010.98130.9810
Poisson300.86170.92210.94880.96700.96620.97690.97730.97860.98030.98330.98500.98530.98550.98600.98750.9873
Uniform0.88870.93670.94520.94680.96440.96550.96590.96950.97170.97870.97870.98160.97840.98350.98310.9845
+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/viv/index.html b/zh/examples/viv/index.html new file mode 100644 index 0000000000..c05586bf0c --- /dev/null +++ b/zh/examples/viv/index.html @@ -0,0 +1,4407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ViV - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

VIV(vortex induced vibration)

+

AI Studio快速体验

+
+
+
+
python viv.py
+
+
+
+
python viv.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/viv/viv_pretrained.pdparams
+
+
+
+
python viv.py mode=export
+
+
+
+
python viv.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
viv_pretrained.pdparams
viv_pretrained.pdeqn
eta_l2/MSE.eta: 0.00875
eta_l2/MSE.f: 0.00921
+

1. 背景简介

+

涡激振动(Vortex-Induced Vibration,VIV)是一种流固耦合振动现象,主要发生在流体绕过柱体或管体时。在海洋工程和风工程中,这种振动现象具有重要应用。

+

在海洋工程中,涡激振动问题主要涉及海洋平台(如桩基、立管等)的涡激振动响应分析。这些平台在海流中运行,会受到涡激振动的影响。这种振动可能会导致平台结构的疲劳损伤,因此在进行海洋平台设计时,需要考虑这一问题。

+

在风工程中,涡激振动问题主要涉及风力发电机的涡激振动响应分析。风力发电机叶片在运行过程中受到气流的涡激振动,这种振动可能会导致叶片的疲劳损伤。为了确保风力发电机的安全运行,需要对这一问题进行深入的研究。

+

总之,涡激振动问题的应用主要涉及海洋工程和风工程领域,对于这些领域的发展具有重要意义。

+

当涡流脱落频率接近结构的固有频率时,圆柱会发生涡激振动,VIV系统相当于一个弹簧-阻尼系统:

+

VIV_1D_SpringDamper

+

2. 问题定义

+

本问题涉及的控制方程涉及三个物理量:\(λ_1\)\(λ_2\)\(ρ\),分别表示自然阻尼、结构特性刚度和质量,控制方程定义如下所示:

+
\[ +\rho \dfrac{\partial^2 \eta}{\partial t^2} + \lambda_1 \dfrac{\partial \eta}{\partial t} + \lambda_2 \eta = f +\]
+

该模型基于无量纲速度 \(U_r=\dfrac{u}{f_n*d}=8.5\) 对应 \(Re=500\) 的假设。我们使用通过圆柱的流体引起的圆柱振动的横向振幅 \(\eta\) 和相应的升力 \(f\) 作为监督数据。

+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在 VIV 问题中,给定时间 \(t\),上述系统都有横向振幅 \(\eta\) 和升力 \(f\) 作为待求解的未知量,并且该系统本身还包含两个参数 \(\lambda_1, \lambda_2\)。因此我们在这里使用比较简单的 MLP(Multilayer Perceptron, 多层感知机) 来表示 \(t\)\((\eta, f)\) 的映射函数 \(g: \mathbb{R}^1 \to \mathbb{R}^2\) ,即:

+
\[ +\eta, f = g(t) +\]
+

上式中 \(g\) 即为 MLP 模型本身,用 PaddleScience 代码表示如下

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 ("t_f",),输出变量名是 ("eta",), + t_f 代表输入时间 \(t\)eta 代表输出振幅 \(\eta\) 这些命名与后续代码保持一致。

+

接着通过指定 MLP 的层数、神经元个数以及激活函数,我们就实例化出了一个拥有 5 层隐藏神经元,每层神经元数为 50,使用 "tanh" 作为激活函数的神经网络模型 model

+

3.2 方程构建

+

由于 VIV 使用的是 VIV 方程,因此可以直接使用 PaddleScience 内置的 VIV

+
# set equation
+equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}
+
+

我们在该方程中添加了两个可学习的参数 k1k2 来估计 \(\lambda_1\)\(\lambda_2\),且它们的关系是 \(\lambda_1 = e^{k1}, \lambda_2 = e^{k2}\)

+

因此我们在实例化 VIV 类时需指定必要的参数:质量 rho=2,初始化值k1=-4k2=0

+

3.3 计算域构建

+

本文中 VIV 问题作用在 \(t \in [0.0625, 9.9375]\) 中的 100 个离散时间点上,这 100 个时间点已经保存在文件 examples/fsi/VIV_Training_Neta100.mat 作为输入数据,因此不需要显式构建计算域。

+

3.4 约束构建

+

本文采用监督学习的方式,对模型输出 \(\eta\) 和基于 \(\eta\) 计算出的升力 \(f\),这两个物理量进行约束。

+

3.4.1 监督约束

+

由于我们以监督学习方式进行训练,此处采用监督约束 SupervisedConstraint

+
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    {
+        "dataset": {
+            "name": "MatDataset",
+            "file_path": cfg.VIV_DATA_PATH,
+            "input_keys": ("t_f",),
+            "label_keys": ("eta", "f"),
+            "weight_dict": {"eta": 100},
+        },
+        "batch_size": cfg.TRAIN.batch_size,
+        "sampler": {
+            "name": "BatchSampler",
+            "drop_last": False,
+            "shuffle": True,
+        },
+    },
+    ppsci.loss.MSELoss("mean"),
+    {"eta": lambda out: out["eta"], **equation["VIV"].equations},
+    name="Sup",
+)
+
+

SupervisedConstraint 的第一个参数是监督约束的读取配置,此处填入在 3.2 方程构建 章节中实例化好的 train_dataloader_cfg

+

第二个参数是损失函数,此处我们选用常用的MSE函数,且 reduction 设置为 "mean",即我们会将参与计算的所有数据点产生的损失项求和取平均;

+

第三个参数是方程表达式,用于描述如何计算约束目标,此处填入 eta 的计算函数和在 3.2 方程构建 章节中实例化好的 equation["VIV"].equations

+

第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。此处我们命名为 "Sup" 即可。

+

在监督约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,使用 10000 轮训练轮数,并每隔 10000 个epochs评估一次模型精度。

+
42
+43
+44
+45
+46
+47
+48
+49
TRAIN:
+  epochs: 100000
+  iters_per_epoch: 1
+  save_freq: 10000
+  eval_during_train: true
+  eval_freq: 1000
+  batch_size: 100
+  lr_scheduler:
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器和 Step 间隔衰减学习率。

+
52
+53
+54
# set optimizer
+lr_scheduler = ppsci.optimizer.lr_scheduler.Step(**cfg.TRAIN.lr_scheduler)()
+optimizer = ppsci.optimizer.Adam(lr_scheduler)((model,) + tuple(equation.values()))
+
+
+说明 +

VIV 方程含有两个 可学习参数 k1和k2,因此需要将方程与 model 一起传入优化器。

+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器。

+
56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
# set validator
+eta_l2_validator = ppsci.validate.SupervisedValidator(
+    {
+        "dataset": {
+            "name": "MatDataset",
+            "file_path": cfg.VIV_DATA_PATH,
+            "input_keys": ("t_f",),
+            "label_keys": ("eta", "f"),
+        },
+        "batch_size": cfg.EVAL.batch_size,
+    },
+    ppsci.loss.MSELoss("mean"),
+    {"eta": lambda out: out["eta"], **equation["VIV"].equations},
+    metric={"MSE": ppsci.metric.L2Rel()},
+    name="eta_l2",
+)
+validator = {eta_l2_validator.name: eta_l2_validator}
+
+

评价指标 metric 选择 ppsci.metric.MSE 即可;

+

其余配置与 3.4.1 监督约束构建 的设置类似。

+

3.8 可视化器构建

+

在模型评估时,如果评估结果是可以可视化的数据,我们可以选择合适的可视化器来对输出结果进行可视化。

+

本文需要可视化的数据是 \(t-\eta\)\(t-f\) 两组关系图,假设每个时刻 \(t\) 的坐标是 \(t_i\),则对应网络输出为 \(\eta_i\),升力为 \(f_i\),因此我们只需要将评估过程中产生的所有 \((t_i, \eta_i, f_i)\) 保存成图片即可。代码如下:

+
74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
# set visualizer(optional)
+visu_mat = ppsci.utils.reader.load_mat_file(
+    cfg.VIV_DATA_PATH,
+    ("t_f", "eta_gt", "f_gt"),
+    alias_dict={"eta_gt": "eta", "f_gt": "f"},
+)
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visu_mat,
+        ("t_f",),
+        {
+            r"$\eta$": lambda d: d["eta"],  # plot with latex title
+            r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
+            r"$f$": equation["VIV"].equations["f"],  # plot with latex title
+            r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
+        },
+        num_timestamps=1,
+        prefix="viv_pred",
+    )
+}
+
+

3.9 模型训练、评估与可视化

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估、可视化。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    optimizer=optimizer,
+    equation=equation,
+    validator=validator,
+    visualizer=visualizer,
+    cfg=cfg,
+)
+
+# train model
+solver.train()
+# evaluate after finished training
+solver.eval()
+# visualize prediction after finished training
+solver.visualize()
+
+

4. 完整代码

+
viv.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hydra
+from omegaconf import DictConfig
+
+import ppsci
+
+
+def train(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        {
+            "dataset": {
+                "name": "MatDataset",
+                "file_path": cfg.VIV_DATA_PATH,
+                "input_keys": ("t_f",),
+                "label_keys": ("eta", "f"),
+                "weight_dict": {"eta": 100},
+            },
+            "batch_size": cfg.TRAIN.batch_size,
+            "sampler": {
+                "name": "BatchSampler",
+                "drop_last": False,
+                "shuffle": True,
+            },
+        },
+        ppsci.loss.MSELoss("mean"),
+        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
+        name="Sup",
+    )
+    # wrap constraints together
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set optimizer
+    lr_scheduler = ppsci.optimizer.lr_scheduler.Step(**cfg.TRAIN.lr_scheduler)()
+    optimizer = ppsci.optimizer.Adam(lr_scheduler)((model,) + tuple(equation.values()))
+
+    # set validator
+    eta_l2_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "MatDataset",
+                "file_path": cfg.VIV_DATA_PATH,
+                "input_keys": ("t_f",),
+                "label_keys": ("eta", "f"),
+            },
+            "batch_size": cfg.EVAL.batch_size,
+        },
+        ppsci.loss.MSELoss("mean"),
+        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
+        metric={"MSE": ppsci.metric.L2Rel()},
+        name="eta_l2",
+    )
+    validator = {eta_l2_validator.name: eta_l2_validator}
+
+    # set visualizer(optional)
+    visu_mat = ppsci.utils.reader.load_mat_file(
+        cfg.VIV_DATA_PATH,
+        ("t_f", "eta_gt", "f_gt"),
+        alias_dict={"eta_gt": "eta", "f_gt": "f"},
+    )
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerScatter1D(
+            visu_mat,
+            ("t_f",),
+            {
+                r"$\eta$": lambda d: d["eta"],  # plot with latex title
+                r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
+                r"$f$": equation["VIV"].equations["f"],  # plot with latex title
+                r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
+            },
+            num_timestamps=1,
+            prefix="viv_pred",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        optimizer=optimizer,
+        equation=equation,
+        validator=validator,
+        visualizer=visualizer,
+        cfg=cfg,
+    )
+
+    # train model
+    solver.train()
+    # evaluate after finished training
+    solver.eval()
+    # visualize prediction after finished training
+    solver.visualize()
+
+
+def evaluate(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set equation
+    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}
+
+    # set validator
+    eta_l2_validator = ppsci.validate.SupervisedValidator(
+        {
+            "dataset": {
+                "name": "MatDataset",
+                "file_path": cfg.VIV_DATA_PATH,
+                "input_keys": ("t_f",),
+                "label_keys": ("eta", "f"),
+            },
+            "batch_size": cfg.EVAL.batch_size,
+        },
+        ppsci.loss.MSELoss("mean"),
+        {"eta": lambda out: out["eta"], **equation["VIV"].equations},
+        metric={"MSE": ppsci.metric.L2Rel()},
+        name="eta_l2",
+    )
+    validator = {eta_l2_validator.name: eta_l2_validator}
+
+    # set visualizer(optional)
+    visu_mat = ppsci.utils.reader.load_mat_file(
+        cfg.VIV_DATA_PATH,
+        ("t_f", "eta_gt", "f_gt"),
+        alias_dict={"eta_gt": "eta", "f_gt": "f"},
+    )
+
+    visualizer = {
+        "visualize_u": ppsci.visualize.VisualizerScatter1D(
+            visu_mat,
+            ("t_f",),
+            {
+                r"$\eta$": lambda d: d["eta"],  # plot with latex title
+                r"$\eta_{gt}$": lambda d: d["eta_gt"],  # plot with latex title
+                r"$f$": equation["VIV"].equations["f"],  # plot with latex title
+                r"$f_{gt}$": lambda d: d["f_gt"],  # plot with latex title
+            },
+            num_timestamps=1,
+            prefix="viv_pred",
+        )
+    }
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        equation=equation,
+        validator=validator,
+        visualizer=visualizer,
+        cfg=cfg,
+    )
+
+    # evaluate
+    solver.eval()
+    # visualize prediction
+    solver.visualize()
+
+
+def export(cfg: DictConfig):
+    from paddle import nn
+    from paddle.static import InputSpec
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+    # initialize equation
+    equation = {"VIV": ppsci.equation.Vibration(2, -4, 0)}
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        equation=equation,
+        cfg=cfg,
+    )
+    # Convert equation to func
+    f_func = ppsci.lambdify(
+        solver.equation["VIV"].equations["f"],
+        solver.model,
+        list(solver.equation["VIV"].learnable_parameters),
+    )
+
+    class Wrapped_Model(nn.Layer):
+        def __init__(self, model, func):
+            super().__init__()
+            self.model = model
+            self.func = func
+
+        def forward(self, x):
+            model_out = self.model(x)
+            func_out = self.func(x)
+            return {**model_out, "f": func_out}
+
+    solver.model = Wrapped_Model(model, f_func)
+    # export models
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path, skip_prune_program=True)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    # set model predictor
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    infer_mat = ppsci.utils.reader.load_mat_file(
+        cfg.VIV_DATA_PATH,
+        ("t_f", "eta_gt", "f_gt"),
+        alias_dict={"eta_gt": "eta", "f_gt": "f"},
+    )
+
+    input_dict = {key: infer_mat[key] for key in cfg.INFER.input_keys}
+
+    output_dict = predictor.predict(input_dict, cfg.INFER.batch_size)
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.INFER.output_keys, output_dict.keys())
+    }
+    infer_mat.update(output_dict)
+
+    ppsci.visualize.plot.save_plot_from_1d_dict(
+        "./viv_pred", infer_mat, ("t_f",), ("eta", "eta_gt", "f", "f_gt")
+    )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="viv.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

模型预测结果如下所示,横轴为时间自变量\(t\)\(\eta_{gt}\)为参考振幅,\(\eta\)为模型预测振幅,\(f_{gt}\)为参考升力,\(f\)为模型预测升力。

+
+

Viv_result +

+
振幅 eta 与升力 f 随时间t变化的预测结果和参考结果
+
+

可以看到模型对在\([0,10]\)时间范围内,对振幅和升力的预测结果与参考结果基本一致。

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/volterra_ide/index.html b/zh/examples/volterra_ide/index.html new file mode 100644 index 0000000000..7f86e23449 --- /dev/null +++ b/zh/examples/volterra_ide/index.html @@ -0,0 +1,4741 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Volterra_IDE - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Volterra integral equation

+

AI Studio快速体验

+
+
+
+
python volterra_ide.py
+
+
+
+
python volterra_ide.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/volterra_ide/volterra_ide_pretrained.pdparams
+
+
+
+
python volterra_ide.py mode=export
+
+
+
+
python volterra_ide.py mode=infer
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
volterra_ide_pretrained.pdparamsloss(L2Rel_Validator): 0.00023
L2Rel.u(L2Rel_Validator): 0.00023
+

1. 背景简介

+

Volterra integral equation(沃尔泰拉积分方程)是一种积分方程,即方程中含有对待求解函数的积分运算,其有两种形式,如下所示

+
\[ +\begin{aligned} + f(t) &= \int_a^t K(t, s) x(s) d s \\ + x(t) &= f(t)+\int_a^t K(t, s) x(s) d s +\end{aligned} +\]
+

在数学领域,沃尔泰拉方程可以用于表达各种多变量概率分布,是进行多变量统计分析的有力工具。这使得它在处理复杂数据结构时非常有用,例如在机器学习领域。沃尔泰拉方程还可以用于计算不同维度属性的相关性,以及模拟复杂的数据集结构,以便为机器学习任务提供有效的数据支持。

+

在生物学领域,沃尔泰拉方程被用作渔业生产的指导,对生态平衡和环境保护有重要意义。此外,该方程还在疾病防治,人口统计等方面有应用。值得一提的是,沃尔泰拉方程的建立是数学在生物学领域应用的首次成功尝试,推动了生物数学这门科学的产生和发展。

+

本案例以第二种方程为例,使用深度学习的方式进行求解。

+

2. 问题定义

+

假设存在如下 IDE 方程:

+
\[ +u(t) = -\dfrac{du}{dt} + \int_{t_0}^t e^{t-s} u(s) d s +\]
+

其中 \(u(t)\) 就是待求解的函数,而 \(-\dfrac{du}{dt}\) 对应了 \(f(t)\)\(e^{t-s}\) 对应了 \(K(t,s)\)。 +因此可以利用神经网络模型,以 \(t\) 为输入,\(u(t)\) 为输出,根据上述方程构建微分约束,进行无监督学习最终拟合出待求解的函数 \(u(t)\)

+

为了方便在计算机中进行求解,我们将上式进行移项,让积分项作为左侧,非积分项放至右侧,如下所示:

+
\[ +\int_{t_0}^t e^{t-s} u(s) d s = u(t) + \dfrac{du}{dt} +\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 模型构建

+

在上述问题中,我们确定了输入为 \(x\),输出为 \(u(x)\),因此我们使用,用 PaddleScience 代码表示如下:

+
# set model
+model = ppsci.arch.MLP(**cfg.MODEL)
+
+

为了在计算时,准确快速地访问具体变量的值,我们在这里指定网络模型的输入变量名是 "x"(即公式中的 \(t\)),输出变量名是 "u",接着通过指定 MLP 的隐藏层层数、神经元个数,我们就实例化出了神经网络模型 model

+

3.2 计算域构建

+

Volterra_IDE 问题的积分域是 \(a\) ~ \(t\),其中 a 为固定常数 0,t 的范围为 0 ~ 5,因此可以使用PaddleScience 内置的一维几何 TimeDomain 作为计算域。

+
# set geometry
+geom = {"timedomain": ppsci.geometry.TimeDomain(*cfg.BOUNDS)}
+
+

3.3 方程构建

+

由于 Volterra_IDE 使用的是积分方程,因此可以直接使用 PaddleScience 内置的 ppsci.equation.Volterra,并指定所需的参数:积分下限 at 的离散取值点数 num_points、一维高斯积分点的个数 quad_deg\(K(t,s)\) 核函数 kernel_func\(u(t) - f(t)\) 等式右侧表达式 func

+
45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
# set equation
+def kernel_func(x, s):
+    return np.exp(s - x)
+
+def func(out):
+    x, u = out["x"], out["u"]
+    return jacobian(u, x) + u
+
+equation = {
+    "volterra": ppsci.equation.Volterra(
+        cfg.BOUNDS[0],
+        cfg.TRAIN.npoint_interior,
+        cfg.TRAIN.quad_deg,
+        kernel_func,
+        func,
+    )
+}
+
+

3.4 约束构建

+

3.4.1 内部点约束

+

本文采用无监督学习的方式,对移项后方程的左、右两侧进行约束,让其尽量相等。

+

由于等式左侧涉及到积分计算(实际采用高斯积分近似计算),因此在 0 ~ 5 区间内采样出多个 t_i 点后,还需要计算其用于高斯积分的点集,即对每一个 (0,t_i) 区间,都计算出一一对应的高斯积分点集 quad_i 和点权 weight_i。PaddleScience 将这一步作为输入数据的预处理,加入到代码中,如下所示

+
# set constraint
+# set transform for input data
+def input_data_quad_transform(
+    input: Dict[str, np.ndarray],
+    weight: Dict[str, np.ndarray],
+    label: Dict[str, np.ndarray],
+) -> Tuple[
+    Dict[str, paddle.Tensor], Dict[str, paddle.Tensor], Dict[str, paddle.Tensor]
+]:
+    """Get sampling points for integral.
+
+    Args:
+        input (Dict[str, paddle.Tensor]): Raw input dict.
+        weight (Dict[str, paddle.Tensor]): Raw weight dict.
+        label (Dict[str, paddle.Tensor]): Raw label dict.
+
+    Returns:
+        Tuple[ Dict[str, paddle.Tensor], Dict[str, paddle.Tensor], Dict[str, paddle.Tensor] ]:
+            Input dict contained sampling points, weight dict and label dict.
+    """
+    x = input["x"]  # N points.
+    x_quad = equation["volterra"].get_quad_points(x).reshape([-1, 1])  # NxQ
+    x_quad = paddle.concat((x, x_quad), axis=0)  # M+MxQ: [M|Q1|Q2,...,QM|]
+    return (
+        {
+            **input,
+            "x": x_quad,
+        },
+        weight,
+        label,
+    )
+
+# interior constraint
+ide_constraint = ppsci.constraint.InteriorConstraint(
+    equation["volterra"].equations,
+    {"volterra": 0},
+    geom["timedomain"],
+    {
+        "dataset": {
+            "name": "IterableNamedArrayDataset",
+            "transforms": (
+                {
+                    "FunctionalTransform": {
+                        "transform_func": input_data_quad_transform,
+                    },
+                },
+            ),
+        },
+        "batch_size": cfg.TRAIN.npoint_interior,
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean"),
+    evenly=True,
+    name="EQ",
+)
+
+

3.4.2 初值约束

+

\(t=0\) 时,有以下初值条件:

+
\[ +u(0) = e^{-t} \cosh(t)|_{t=0} = e^{0} \cosh(0) = 1 +\]
+

因此可以加入 t=0 时的初值条件,代码如下所示

+
# initial condition
+def u_solution_func(in_):
+    if isinstance(in_["x"], paddle.Tensor):
+        return paddle.exp(-in_["x"]) * paddle.cosh(in_["x"])
+    return np.exp(-in_["x"]) * np.cosh(in_["x"])
+
+ic = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda out: out["u"]},
+    {"u": u_solution_func},
+    geom["timedomain"],
+    {
+        "dataset": {"name": "IterableNamedArrayDataset"},
+        "batch_size": cfg.TRAIN.npoint_ic,
+        "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+    },
+    ppsci.loss.MSELoss("mean"),
+    criteria=geom["timedomain"].on_initial,
+    name="IC",
+)
+
+

在微分方程约束、初值约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。

+
# wrap constraints together
+constraint = {
+    ide_constraint.name: ide_constraint,
+    ic.name: ic,
+}
+
+

3.5 超参数设定

+

接下来我们需要指定训练轮数和学习率,此处我们按实验经验,让 L-BFGS 优化器进行一轮优化即可,但一轮优化内的 max_iters 数可以设置为一个较大的一个数 15000

+
39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
# training settings
+TRAIN:
+  epochs: 1
+  iters_per_epoch: 1
+  save_freq: 1
+  eval_during_train: true
+  eval_freq: 1
+  optimizer:
+    learning_rate: 1
+    max_iter: 15000
+    max_eval: 1250
+    tolerance_grad: 1.0e-8
+    tolerance_change: 0
+    history_size: 100
+  quad_deg: 20
+  npoint_interior: 12
+  npoint_ic: 1
+  pretrained_model_path: null
+  checkpoint_path: null
+
+

3.6 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 LBFGS 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.LBFGS(**cfg.TRAIN.optimizer)(model)
+
+

3.7 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.GeometryValidator 构建评估器。

+
# set validator
+l2rel_validator = ppsci.validate.GeometryValidator(
+    {"u": lambda out: out["u"]},
+    {"u": u_solution_func},
+    geom["timedomain"],
+    {
+        "dataset": "IterableNamedArrayDataset",
+        "total_size": cfg.EVAL.npoint_eval,
+    },
+    ppsci.loss.L2RelLoss(),
+    evenly=True,
+    metric={"L2Rel": ppsci.metric.L2Rel()},
+    name="L2Rel_Validator",
+)
+validator = {l2rel_validator.name: l2rel_validator}
+
+

评价指标 metric 选择 ppsci.metric.L2Rel 即可。

+

其余配置与 3.4 约束构建 的设置类似。

+

3.8 模型训练

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    cfg.output_dir,
+    optimizer,
+    epochs=cfg.TRAIN.epochs,
+    iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+    eval_during_train=cfg.TRAIN.eval_during_train,
+    eval_freq=cfg.TRAIN.eval_freq,
+    equation=equation,
+    geom=geom,
+    validator=validator,
+    pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+    checkpoint_path=cfg.TRAIN.checkpoint_path,
+    eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+)
+# train model
+solver.train()
+
+

3.9 结果可视化

+

在模型训练完毕之后,我们可以手动构造 0 ~ 5 区间内均匀 100 个点,作为评估的积分上限 t 进行预测,并可视化结果。

+
# visualize prediction after finished training
+input_data = geom["timedomain"].uniform_points(100)
+label_data = u_solution_func({"x": input_data})
+output_data = solver.predict({"x": input_data}, return_numpy=True)["u"]
+
+plt.plot(input_data, label_data, "-", label=r"$u(t)$")
+plt.plot(input_data, output_data, "o", label=r"$\hat{u}(t)$", markersize=4.0)
+plt.legend()
+plt.xlabel(r"$t$")
+plt.ylabel(r"$u$")
+plt.title(r"$u-t$")
+plt.savefig(osp.join(cfg.output_dir, "./Volterra_IDE.png"), dpi=200)
+
+

4. 完整代码

+
volterra_ide.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Reference: https://github.com/lululxvi/deepxde/blob/master/examples/pinn_forward/Volterra_IDE.py
+
+from os import path as osp
+from typing import Dict
+from typing import Tuple
+
+import hydra
+import numpy as np
+import paddle
+from matplotlib import pyplot as plt
+from omegaconf import DictConfig
+
+import ppsci
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+
+def train(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # set output directory
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "train.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set geometry
+    geom = {"timedomain": ppsci.geometry.TimeDomain(*cfg.BOUNDS)}
+
+    # set equation
+    def kernel_func(x, s):
+        return np.exp(s - x)
+
+    def func(out):
+        x, u = out["x"], out["u"]
+        return jacobian(u, x) + u
+
+    equation = {
+        "volterra": ppsci.equation.Volterra(
+            cfg.BOUNDS[0],
+            cfg.TRAIN.npoint_interior,
+            cfg.TRAIN.quad_deg,
+            kernel_func,
+            func,
+        )
+    }
+
+    # set constraint
+    # set transform for input data
+    def input_data_quad_transform(
+        input: Dict[str, np.ndarray],
+        weight: Dict[str, np.ndarray],
+        label: Dict[str, np.ndarray],
+    ) -> Tuple[
+        Dict[str, paddle.Tensor], Dict[str, paddle.Tensor], Dict[str, paddle.Tensor]
+    ]:
+        """Get sampling points for integral.
+
+        Args:
+            input (Dict[str, paddle.Tensor]): Raw input dict.
+            weight (Dict[str, paddle.Tensor]): Raw weight dict.
+            label (Dict[str, paddle.Tensor]): Raw label dict.
+
+        Returns:
+            Tuple[ Dict[str, paddle.Tensor], Dict[str, paddle.Tensor], Dict[str, paddle.Tensor] ]:
+                Input dict contained sampling points, weight dict and label dict.
+        """
+        x = input["x"]  # N points.
+        x_quad = equation["volterra"].get_quad_points(x).reshape([-1, 1])  # NxQ
+        x_quad = paddle.concat((x, x_quad), axis=0)  # M+MxQ: [M|Q1|Q2,...,QM|]
+        return (
+            {
+                **input,
+                "x": x_quad,
+            },
+            weight,
+            label,
+        )
+
+    # interior constraint
+    ide_constraint = ppsci.constraint.InteriorConstraint(
+        equation["volterra"].equations,
+        {"volterra": 0},
+        geom["timedomain"],
+        {
+            "dataset": {
+                "name": "IterableNamedArrayDataset",
+                "transforms": (
+                    {
+                        "FunctionalTransform": {
+                            "transform_func": input_data_quad_transform,
+                        },
+                    },
+                ),
+            },
+            "batch_size": cfg.TRAIN.npoint_interior,
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean"),
+        evenly=True,
+        name="EQ",
+    )
+
+    # initial condition
+    def u_solution_func(in_):
+        if isinstance(in_["x"], paddle.Tensor):
+            return paddle.exp(-in_["x"]) * paddle.cosh(in_["x"])
+        return np.exp(-in_["x"]) * np.cosh(in_["x"])
+
+    ic = ppsci.constraint.BoundaryConstraint(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["timedomain"],
+        {
+            "dataset": {"name": "IterableNamedArrayDataset"},
+            "batch_size": cfg.TRAIN.npoint_ic,
+            "iters_per_epoch": cfg.TRAIN.iters_per_epoch,
+        },
+        ppsci.loss.MSELoss("mean"),
+        criteria=geom["timedomain"].on_initial,
+        name="IC",
+    )
+    # wrap constraints together
+    constraint = {
+        ide_constraint.name: ide_constraint,
+        ic.name: ic,
+    }
+
+    # set optimizer
+    optimizer = ppsci.optimizer.LBFGS(**cfg.TRAIN.optimizer)(model)
+
+    # set validator
+    l2rel_validator = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["timedomain"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": cfg.EVAL.npoint_eval,
+        },
+        ppsci.loss.L2RelLoss(),
+        evenly=True,
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="L2Rel_Validator",
+    )
+    validator = {l2rel_validator.name: l2rel_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        constraint,
+        cfg.output_dir,
+        optimizer,
+        epochs=cfg.TRAIN.epochs,
+        iters_per_epoch=cfg.TRAIN.iters_per_epoch,
+        eval_during_train=cfg.TRAIN.eval_during_train,
+        eval_freq=cfg.TRAIN.eval_freq,
+        equation=equation,
+        geom=geom,
+        validator=validator,
+        pretrained_model_path=cfg.TRAIN.pretrained_model_path,
+        checkpoint_path=cfg.TRAIN.checkpoint_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # train model
+    solver.train()
+
+    # visualize prediction after finished training
+    input_data = geom["timedomain"].uniform_points(100)
+    label_data = u_solution_func({"x": input_data})
+    output_data = solver.predict({"x": input_data}, return_numpy=True)["u"]
+
+    plt.plot(input_data, label_data, "-", label=r"$u(t)$")
+    plt.plot(input_data, output_data, "o", label=r"$\hat{u}(t)$", markersize=4.0)
+    plt.legend()
+    plt.xlabel(r"$t$")
+    plt.ylabel(r"$u$")
+    plt.title(r"$u-t$")
+    plt.savefig(osp.join(cfg.output_dir, "./Volterra_IDE.png"), dpi=200)
+
+
+def evaluate(cfg: DictConfig):
+    # set random seed for reproducibility
+    ppsci.utils.misc.set_random_seed(cfg.seed)
+
+    # set output directory
+    logger.init_logger("ppsci", osp.join(cfg.output_dir, "eval.log"), "info")
+
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # set geometry
+    geom = {"timedomain": ppsci.geometry.TimeDomain(*cfg.BOUNDS)}
+    # set validator
+
+    def u_solution_func(in_) -> np.ndarray:
+        if isinstance(in_["x"], paddle.Tensor):
+            return paddle.exp(-in_["x"]) * paddle.cosh(in_["x"])
+        return np.exp(-in_["x"]) * np.cosh(in_["x"])
+
+    l2rel_validator = ppsci.validate.GeometryValidator(
+        {"u": lambda out: out["u"]},
+        {"u": u_solution_func},
+        geom["timedomain"],
+        {
+            "dataset": "IterableNamedArrayDataset",
+            "total_size": cfg.EVAL.npoint_eval,
+        },
+        ppsci.loss.L2RelLoss(),
+        evenly=True,
+        metric={"L2Rel": ppsci.metric.L2Rel()},
+        name="L2Rel_Validator",
+    )
+    validator = {l2rel_validator.name: l2rel_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        output_dir=cfg.output_dir,
+        geom=geom,
+        validator=validator,
+        pretrained_model_path=cfg.EVAL.pretrained_model_path,
+        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
+    )
+    # evaluate model
+    solver.eval()
+
+    # visualize prediction
+    input_data = geom["timedomain"].uniform_points(cfg.EVAL.npoint_eval)
+    label_data = u_solution_func({"x": input_data})
+    output_data = solver.predict({"x": input_data}, return_numpy=True)["u"]
+
+    plt.plot(input_data, label_data, "-", label=r"$u(t)$")
+    plt.plot(input_data, output_data, "o", label=r"$\hat{u}(t)$", markersize=4.0)
+    plt.legend()
+    plt.xlabel(r"$t$")
+    plt.ylabel(r"$u$")
+    plt.title(r"$u-t$")
+    plt.savefig(osp.join(cfg.output_dir, "./Volterra_IDE.png"), dpi=200)
+
+
+def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {
+            key: InputSpec([None, 1], "float32", name=key)
+            for key in cfg.MODEL.input_keys
+        },
+    ]
+    solver.export(input_spec, cfg.INFER.export_path)
+
+
+def inference(cfg: DictConfig):
+    from deploy.python_infer import pinn_predictor
+
+    predictor = pinn_predictor.PINNPredictor(cfg)
+
+    # set geometry
+    geom = {"timedomain": ppsci.geometry.TimeDomain(*cfg.BOUNDS)}
+
+    input_data = geom["timedomain"].uniform_points(cfg.EVAL.npoint_eval)
+    input_dict = {"x": input_data}
+
+    output_dict = predictor.predict(
+        {key: input_dict[key] for key in cfg.MODEL.input_keys}, cfg.INFER.batch_size
+    )
+
+    # mapping data to cfg.INFER.output_keys
+    output_dict = {
+        store_key: output_dict[infer_key]
+        for store_key, infer_key in zip(cfg.MODEL.output_keys, output_dict.keys())
+    }
+
+    def u_solution_func(in_) -> np.ndarray:
+        if isinstance(in_["x"], paddle.Tensor):
+            return paddle.exp(-in_["x"]) * paddle.cosh(in_["x"])
+        return np.exp(-in_["x"]) * np.cosh(in_["x"])
+
+    label_data = u_solution_func({"x": input_data})
+    output_data = output_dict["u"]
+
+    # save result
+    plt.plot(input_data, label_data, "-", label=r"$u(t)$")
+    plt.plot(input_data, output_data, "o", label=r"$\hat{u}(t)$", markersize=4.0)
+    plt.legend()
+    plt.xlabel(r"$t$")
+    plt.ylabel(r"$u$")
+    plt.title(r"$u-t$")
+    plt.savefig("./Volterra_IDE_pred.png", dpi=200)
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="volterra_ide.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    elif cfg.mode == "export":
+        export(cfg)
+    elif cfg.mode == "infer":
+        inference(cfg)
+    else:
+        raise ValueError(
+            f"cfg.mode should in ['train', 'eval', 'export', 'infer'], but got '{cfg.mode}'"
+        )
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

模型预测结果如下所示,\(t\)为自变量,\(u(t)\)为积分方程标准解函数,\(\hat{u}(t)\)为模型预测的积分方程解函数

+
+

result +

+
模型求解结果(橙色散点)和参考结果(蓝色曲线)
+
+

可以看到模型对积分方程在\([0,5]\)区间内的预测结果\(\hat{u}(t)\)和标准解结果\(u(t)\)基本一致。

+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/examples/xpinns/index.html b/zh/examples/xpinns/index.html new file mode 100644 index 0000000000..226c6be7b6 --- /dev/null +++ b/zh/examples/xpinns/index.html @@ -0,0 +1,4894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XPINN - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Extended Physics-Informed Neural Networks (XPINNs)

+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/XPINN/XPINN_2D_PoissonEqn.mat -P ./data/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/XPINN/XPINN_2D_PoissonEqn.mat --create-dirs -o ./data/XPINN_2D_PoissonEqn.mat
+python xpinn.py
+
+
+
+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/XPINN/XPINN_2D_PoissonEqn.mat -P ./data/
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/XPINN/XPINN_2D_PoissonEqn.mat --create-dirs -o ./data/XPINN_2D_PoissonEqn.mat
+python xpinn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/XPINN/xpinn_pretrained.pdparams
+
+
+
+
+ + + + + + + + + + + + + +
预训练模型指标
xpinn_pretrained.pdparamsL2Rel.l2_error: 0.04226
+

1. 背景简介

+

求解偏微分方程(PDE)是一类基础的物理问题,随着人工智能技术的高速发展,利用深度学习求解偏微分方程成为新的研究趋势。XPINNs(Extended Physics-Informed Neural Networks)是一种适用于物理信息神经网络(PINNs)的广义时空域分解方法,以求解任意复杂几何域上的非线性偏微分方程。

+

XPINNs 通过广义时空区域分解,有效地提高了模型的并行能力,并且支持高度不规则的、凸/非凸的时空域分解,界面条件是简单的。XPINNs 可扩展到任意类型的偏微分方程,而不论方程是何种物理性质。

+

精确求解高维复杂的方程已经成为科学计算的最大挑战之一,XPINNs 的优点使其成为模拟复杂方程的适用方法。

+

2. 问题定义

+

二维泊松方程:

+
\[ \Delta u = f(x, y), x,y \in \Omega \subset R^2\]
+

3. 问题求解

+

接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 API文档

+

3.1 数据集下载

+

如下图所示,数据集包含计算域的三个子区域的数据:红色区域的边界和残差点;黄色区域的界面;以及绿色区域的界面。

+
+

+

+
二维泊松方程的三个子区域
+
+

计算域的边界表达式如下。

+
\[ \gamma =1.5+0.14 sin(4θ)+0.12 cos(6θ)+0.09 cos(5θ), θ \in [0,2π) \]
+

红色区域和黄色区域的界面的表达式如下。

+
\[ \gamma_1 =0.5+0.18 sin(3θ)+0.08 cos(2θ)+0.2 cos(5θ), θ \in [0,2π)\]
+
\[ \gamma_2 =0.34+0.04 sin(5θ)+0.18 cos(3θ)+0.1 cos(6θ), θ \in [0,2π) \]
+

执行以下命令,下载并解压数据集。

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/XPINN/XPINN_2D_PoissonEqn.mat -P ./data/
+
+

3.2 模型构建

+

在本问题中,我们使用神经网络 MLP 作为模型,在模型代码中定义三个 MLP ,分别作为三个子区域的模型。

+
# set model
+custom_model = model.Model(layer_list)
+
+

模型训练时,我们将使用 XPINN 方法分别计算每个子区域的模型损失。

+
+

+

+
XPINN子网络的训练过程
+
+

3.3 约束构建

+

在本案例中,我们使用监督数据集对模型进行训练,因此需要构建监督约束。

+

在定义约束之前,我们需要指定数据集的路径等相关配置,将这些信息存放到对应的 YAML 文件中,如下所示。

+
# set training data file
+DATA_FILE: "./data/XPINN_2D_PoissonEqn.mat"
+
+

接着定义训练损失函数的计算过程,调用 XPINN 方法计算损失,如下所示。

+
def loss_fun(
+    output_dict: Dict[str, paddle.Tensor],
+    label_dict: Dict[str, paddle.Tensor],
+    *args,
+) -> float:
+    def residual_func(output_der: paddle.Tensor, input: paddle.Tensor) -> paddle.Tensor:
+        return paddle.add_n(output_der) - paddle.add_n(
+            [paddle.exp(_in) for _in in input]
+        )
+
+    # subdomain 1
+    loss1 = _xpinn_loss(
+        training_pres=[output_dict["boundary_u"]],
+        training_exacts=[label_dict["boundary_u_exact"]],
+        training_weight=20,
+        residual_inputs=[[output_dict["residual1_x"], output_dict["residual1_y"]]],
+        residual_pres=[output_dict["residual1_u"]],
+        residual_weight=1,
+        interface_inputs=[
+            [output_dict["interface1_x"], output_dict["interface1_y"]],
+            [output_dict["interface2_x"], output_dict["interface2_y"]],
+        ],
+        interface_pres=[
+            output_dict["interface1_u_sub1"],
+            output_dict["interface2_u_sub1"],
+        ],
+        interface_weight=20,
+        interface_neigh_pres=[
+            [output_dict["interface1_u_sub2"]],
+            [output_dict["interface2_u_sub3"]],
+        ],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    # subdomain 2
+    loss2 = _xpinn_loss(
+        residual_inputs=[[output_dict["residual2_x"], output_dict["residual2_y"]]],
+        residual_pres=[output_dict["residual2_u"]],
+        residual_weight=1,
+        interface_inputs=[[output_dict["interface1_x"], output_dict["interface1_y"]]],
+        interface_pres=[output_dict["interface1_u_sub1"]],
+        interface_weight=20,
+        interface_neigh_pres=[[output_dict["interface1_u_sub2"]]],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    # subdomain 3
+    loss3 = _xpinn_loss(
+        residual_inputs=[[output_dict["residual3_x"], output_dict["residual3_y"]]],
+        residual_pres=[output_dict["residual3_u"]],
+        residual_weight=1,
+        interface_inputs=[[output_dict["interface2_x"], output_dict["interface2_y"]]],
+        interface_pres=[output_dict["interface2_u_sub1"]],
+        interface_weight=20,
+        interface_neigh_pres=[[output_dict["interface2_u_sub3"]]],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    return {"residuals": loss1 + loss2 + loss3}
+
+

最后构建监督约束,如下所示。

+
# set constraint
+sup_constraint = ppsci.constraint.SupervisedConstraint(
+    train_dataloader_cfg,
+    ppsci.loss.FunctionalLoss(loss_fun),
+    {"residual1_u": lambda out: out["residual1_u"]},
+    name="sup_constraint",
+)
+constraint = {sup_constraint.name: sup_constraint}
+
+

3.4 超参数设定

+

设置训练轮数等参数,如下所示。

+
84
+85
+86
+87
+88
+89
epochs: 501
+iters_per_epoch: 1
+save_freq: 50
+eval_during_train: true
+eval_freq: 50
+learning_rate: 0.0008
+
+

3.5 优化器构建

+

训练过程会调用优化器来更新模型参数,此处选择较为常用的 Adam 优化器。

+
# set optimizer
+optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(custom_model)
+
+

3.6 评估器构建

+

在训练过程中通常会按一定轮数间隔,用验证集(测试集)评估当前模型的训练情况,因此使用 ppsci.validate.SupervisedValidator 构建评估器。

+
sup_validator = ppsci.validate.SupervisedValidator(
+    eval_dataloader_cfg,
+    loss=ppsci.loss.FunctionalLoss(loss_fun),
+    output_expr={
+        "residual1_u": lambda out: out["residual1_u"],
+        "residual2_u": lambda out: out["residual2_u"],
+        "residual3_u": lambda out: out["residual3_u"],
+    },
+    metric={"L2Rel": ppsci.metric.FunctionalMetric(eval_l2_rel_func)},
+    name="sup_validator",
+)
+validator = {sup_validator.name: sup_validator}
+
+

评估指标为预测结果和真实结果的 L2 相对误差值,这里需自定义指标计算函数,如下所示。

+
def eval_l2_rel_func(
+    output_dict: Dict[str, paddle.Tensor],
+    label_dict: Dict[str, paddle.Tensor],
+    *args,
+) -> Dict[str, paddle.Tensor]:
+    u_pred = paddle.concat(
+        [
+            output_dict["residual1_u"],
+            output_dict["residual2_u"],
+            output_dict["residual3_u"],
+        ]
+    )
+
+    # the shape of label_dict["residual_u_exact"] is [22387, 1], and be cut into [18211, 1] `_eval_by_dataset`(ppsci/solver/eval.py).
+    u_exact = paddle.concat(
+        [
+            label_dict["residual_u_exact"],
+            label_dict["residual2_u_exact"],
+            label_dict["residual3_u_exact"],
+        ]
+    )
+
+    error_total = paddle.linalg.norm(
+        u_exact.flatten() - u_pred.flatten(), 2
+    ) / paddle.linalg.norm(u_exact.flatten(), 2)
+    return {"l2_error": error_total}
+
+

3.7 模型训练评估

+

完成上述设置之后,只需要将上述实例化的对象按顺序传递给 ppsci.solver.Solver,然后启动训练、评估。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    custom_model,
+    constraint,
+    optimizer=optimizer,
+    validator=validator,
+    cfg=cfg,
+)
+
+solver.train()
+solver.eval()
+
+

3.8 结果可视化

+

训练完毕之后程序会对测试集中的数据进行预测,并以图片的形式对结果进行可视化,如下所示。

+
# visualize prediction
+with solver.no_grad_context_manager(True):
+    for index, (_input, _label, _) in enumerate(sup_validator.data_loader):
+        u_exact = _label["residual_u_exact"]
+        output_ = custom_model(_input)
+        u_pred = paddle.concat(
+            [output_["residual1_u"], output_["residual2_u"], output_["residual3_u"]]
+        )
+
+        plotting.log_image(
+            residual1_x=_input["residual1_x"],
+            residual1_y=_input["residual1_y"],
+            residual2_x=_input["residual2_x"],
+            residual2_y=_input["residual2_y"],
+            residual3_x=_input["residual3_x"],
+            residual3_y=_input["residual3_y"],
+            interface1_x=_input["interface1_x"],
+            interface1_y=_input["interface1_y"],
+            interface2_x=_input["interface2_x"],
+            interface2_y=_input["interface2_y"],
+            boundary_x=_input["boundary_x"],
+            boundary_y=_input["boundary_y"],
+            residual_u_pred=u_pred,
+            residual_u_exact=u_exact,
+        )
+
+

4. 完整代码

+
xpinn.py
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Tuple
+
+import hydra
+import model
+import numpy as np
+import paddle
+import plotting
+from omegaconf import DictConfig
+
+import ppsci
+
+# For the use of the second derivative: paddle.cos
+paddle.framework.core.set_prim_eager_enabled(True)
+
+
+def _xpinn_loss(
+    training_pres: List[List[paddle.Tensor]] = None,
+    training_exacts: List[paddle.Tensor] = None,
+    training_weight: float = 1,
+    residual_inputs: List[List[paddle.Tensor]] = None,
+    residual_pres: List[paddle.Tensor] = None,
+    residual_weight: float = 1,
+    interface_inputs: List[List[paddle.Tensor]] = None,
+    interface_pres: List[paddle.Tensor] = None,
+    interface_weight: float = 1,
+    interface_neigh_pres: List[List[paddle.Tensor]] = None,
+    interface_neigh_weight: float = 1,
+    residual_func: Callable = lambda x, y: x - y,
+) -> float:
+    """XPINNs loss function for subdomain
+
+        `loss = W_u_q * MSE_u_q + W_F_q * MSE_F_q + W_I_q * MSE_avg_q + W_I_F_q * MSE_R`
+
+        `W_u_q * MSE_u_q` is data mismatch item.
+        `W_F_q * MSE_F_q` is residual item.
+        `W_I_q * MSE_avg_q` is interface item.
+        `W_I_F_q * MSE_R` is interface residual item.
+
+    Args:
+        training_pres (List[List[paddle.Tensor]], optional): the prediction result for training points input. Defaults to None.
+        training_exacts (List[paddle.Tensor], optional): the exact result for training points input. Defaults to None.
+        training_weight (float, optional): the weight of data mismatch item. Defaults to 1.
+        residual_inputs (List[List[paddle.Tensor]], optional): residual points input. Defaults to None.
+        residual_pres (List[paddle.Tensor], optional): the prediction result for residual points input. Defaults to None.
+        residual_weight (float, optional): the weight of residual item. Defaults to 1.
+        interface_inputs (List[List[paddle.Tensor]], optional): the prediction result for interface points input. Defaults to None.
+        interface_pres (List[paddle.Tensor], optional): the prediction result for interface points input. Defaults to None.
+        interface_weight (float, optional): the weight of iinterface item. Defaults to 1.
+        interface_neigh_pres (List[List[paddle.Tensor]], optional): the prediction result of neighbouring subdomain model for interface points input. Defaults to None.
+        interface_neigh_weight (float, optional): the weight of interface residual term. Defaults to 1.
+        residual_func (Callable, optional): residual calculation  function. Defaults to lambda x,y : x - y.
+    """
+
+    def _get_grad(outputs: paddle.Tensor, inputs: paddle.Tensor) -> paddle.Tensor:
+        grad = paddle.grad(outputs, inputs, retain_graph=True, create_graph=True)
+        return grad[0]
+
+    def _get_second_derivatives(
+        outputs_list: List[paddle.Tensor],
+        inputs_list: List[List[paddle.Tensor]],
+    ) -> Tuple[List[List[paddle.Tensor]], List[List[paddle.Tensor]]]:
+        d1_list = [
+            [_get_grad(_out, _in) for _in in _ins]
+            for _out, _ins in zip(outputs_list, inputs_list)
+        ]
+        d2_list = [
+            [_get_grad(_d1, _in) for _d1, _in in zip(d1s_, _ins)]
+            for d1s_, _ins in zip(d1_list, inputs_list)
+        ]
+        return d2_list
+
+    residual_u_d2_list = _get_second_derivatives(residual_pres, residual_inputs)
+    interface_u_d2_list = _get_second_derivatives(interface_pres, interface_inputs)
+    interface_neigh_u_d2_list = _get_second_derivatives(
+        interface_neigh_pres, interface_inputs
+    )
+
+    MSE_u_q = 0
+
+    if training_pres is not None:
+        for _pre, _exact in zip(training_pres, training_exacts):
+            MSE_u_q += training_weight * paddle.mean(paddle.square(_pre - _exact))
+
+    MSE_F_q = 0
+
+    if residual_inputs is not None:
+        for _ins, _d2 in zip(residual_inputs, residual_u_d2_list):
+            MSE_F_q += residual_weight * paddle.mean(
+                paddle.square(residual_func(_d2, _ins))
+            )
+
+    MSE_avg_q = 0
+    MSE_R = 0
+
+    if interface_inputs is not None:
+        for _ins, _pre, _n_pres in zip(
+            interface_inputs, interface_pres, interface_neigh_pres
+        ):
+            pre_list = [_pre] + _n_pres
+            pre_avg = paddle.add_n(pre_list) / len(pre_list)
+            MSE_avg_q += interface_weight * paddle.mean(paddle.square(_pre - pre_avg))
+
+        for _ins, _d2, _n_d2 in zip(
+            interface_inputs, interface_u_d2_list, interface_neigh_u_d2_list
+        ):
+            MSE_R += interface_neigh_weight * paddle.mean(
+                paddle.square(residual_func(_d2, _ins) - residual_func(_n_d2, _ins))
+            )
+
+    return MSE_u_q + MSE_F_q + MSE_avg_q + MSE_R
+
+
+def loss_fun(
+    output_dict: Dict[str, paddle.Tensor],
+    label_dict: Dict[str, paddle.Tensor],
+    *args,
+) -> float:
+    def residual_func(output_der: paddle.Tensor, input: paddle.Tensor) -> paddle.Tensor:
+        return paddle.add_n(output_der) - paddle.add_n(
+            [paddle.exp(_in) for _in in input]
+        )
+
+    # subdomain 1
+    loss1 = _xpinn_loss(
+        training_pres=[output_dict["boundary_u"]],
+        training_exacts=[label_dict["boundary_u_exact"]],
+        training_weight=20,
+        residual_inputs=[[output_dict["residual1_x"], output_dict["residual1_y"]]],
+        residual_pres=[output_dict["residual1_u"]],
+        residual_weight=1,
+        interface_inputs=[
+            [output_dict["interface1_x"], output_dict["interface1_y"]],
+            [output_dict["interface2_x"], output_dict["interface2_y"]],
+        ],
+        interface_pres=[
+            output_dict["interface1_u_sub1"],
+            output_dict["interface2_u_sub1"],
+        ],
+        interface_weight=20,
+        interface_neigh_pres=[
+            [output_dict["interface1_u_sub2"]],
+            [output_dict["interface2_u_sub3"]],
+        ],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    # subdomain 2
+    loss2 = _xpinn_loss(
+        residual_inputs=[[output_dict["residual2_x"], output_dict["residual2_y"]]],
+        residual_pres=[output_dict["residual2_u"]],
+        residual_weight=1,
+        interface_inputs=[[output_dict["interface1_x"], output_dict["interface1_y"]]],
+        interface_pres=[output_dict["interface1_u_sub1"]],
+        interface_weight=20,
+        interface_neigh_pres=[[output_dict["interface1_u_sub2"]]],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    # subdomain 3
+    loss3 = _xpinn_loss(
+        residual_inputs=[[output_dict["residual3_x"], output_dict["residual3_y"]]],
+        residual_pres=[output_dict["residual3_u"]],
+        residual_weight=1,
+        interface_inputs=[[output_dict["interface2_x"], output_dict["interface2_y"]]],
+        interface_pres=[output_dict["interface2_u_sub1"]],
+        interface_weight=20,
+        interface_neigh_pres=[[output_dict["interface2_u_sub3"]]],
+        interface_neigh_weight=1,
+        residual_func=residual_func,
+    )
+
+    return {"residuals": loss1 + loss2 + loss3}
+
+
+def eval_l2_rel_func(
+    output_dict: Dict[str, paddle.Tensor],
+    label_dict: Dict[str, paddle.Tensor],
+    *args,
+) -> Dict[str, paddle.Tensor]:
+    u_pred = paddle.concat(
+        [
+            output_dict["residual1_u"],
+            output_dict["residual2_u"],
+            output_dict["residual3_u"],
+        ]
+    )
+
+    # the shape of label_dict["residual_u_exact"] is [22387, 1], and be cut into [18211, 1] `_eval_by_dataset`(ppsci/solver/eval.py).
+    u_exact = paddle.concat(
+        [
+            label_dict["residual_u_exact"],
+            label_dict["residual2_u_exact"],
+            label_dict["residual3_u_exact"],
+        ]
+    )
+
+    error_total = paddle.linalg.norm(
+        u_exact.flatten() - u_pred.flatten(), 2
+    ) / paddle.linalg.norm(u_exact.flatten(), 2)
+    return {"l2_error": error_total}
+
+
+def train(cfg: DictConfig):
+    # set training dataset transformation
+    def train_dataset_transform_func(
+        _input: Dict[str, np.ndarray],
+        _label: Dict[str, np.ndarray],
+        weight_: Dict[str, np.ndarray],
+    ) -> Dict[str, np.ndarray]:
+        # Randomly select the residual points from sub-domains
+        id_x1 = np.random.choice(
+            _input["residual1_x"].shape[0],
+            cfg.MODEL.num_residual1_points,
+            replace=False,
+        )
+        _input["residual1_x"] = _input["residual1_x"][id_x1, :]
+        _input["residual1_y"] = _input["residual1_y"][id_x1, :]
+
+        id_x2 = np.random.choice(
+            _input["residual2_x"].shape[0],
+            cfg.MODEL.num_residual2_points,
+            replace=False,
+        )
+        _input["residual2_x"] = _input["residual2_x"][id_x2, :]
+        _input["residual2_y"] = _input["residual2_y"][id_x2, :]
+
+        id_x3 = np.random.choice(
+            _input["residual3_x"].shape[0],
+            cfg.MODEL.num_residual3_points,
+            replace=False,
+        )
+        _input["residual3_x"] = _input["residual3_x"][id_x3, :]
+        _input["residual3_y"] = _input["residual3_y"][id_x3, :]
+
+        # Randomly select boundary points
+        id_x4 = np.random.choice(
+            _input["boundary_x"].shape[0], cfg.MODEL.num_boundary_points, replace=False
+        )
+        _input["boundary_x"] = _input["boundary_x"][id_x4, :]
+        _input["boundary_y"] = _input["boundary_y"][id_x4, :]
+        _label["boundary_u_exact"] = _label["boundary_u_exact"][id_x4, :]
+
+        # Randomly select the interface points along two interfaces
+        id_xi1 = np.random.choice(
+            _input["interface1_x"].shape[0], cfg.MODEL.num_interface1, replace=False
+        )
+        _input["interface1_x"] = _input["interface1_x"][id_xi1, :]
+        _input["interface1_y"] = _input["interface1_y"][id_xi1, :]
+
+        id_xi2 = np.random.choice(
+            _input["interface2_x"].shape[0], cfg.MODEL.num_interface2, replace=False
+        )
+        _input["interface2_x"] = _input["interface2_x"][id_xi2, :]
+        _input["interface2_y"] = _input["interface2_y"][id_xi2, :]
+
+        return _input, _label, weight_
+
+    # set dataloader config
+    train_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATA_FILE,
+            "input_keys": cfg.TRAIN.input_keys,
+            "label_keys": cfg.TRAIN.label_keys,
+            "alias_dict": cfg.TRAIN.alias_dict,
+            "transforms": (
+                {
+                    "FunctionalTransform": {
+                        "transform_func": train_dataset_transform_func,
+                    },
+                },
+            ),
+        }
+    }
+
+    layer_list = (
+        cfg.MODEL.layers1,
+        cfg.MODEL.layers2,
+        cfg.MODEL.layers3,
+    )
+
+    # set model
+    custom_model = model.Model(layer_list)
+
+    # set constraint
+    sup_constraint = ppsci.constraint.SupervisedConstraint(
+        train_dataloader_cfg,
+        ppsci.loss.FunctionalLoss(loss_fun),
+        {"residual1_u": lambda out: out["residual1_u"]},
+        name="sup_constraint",
+    )
+    constraint = {sup_constraint.name: sup_constraint}
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATA_FILE,
+            "input_keys": cfg.TRAIN.input_keys,
+            "label_keys": cfg.EVAL.label_keys,
+            "alias_dict": cfg.EVAL.alias_dict,
+        }
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(loss_fun),
+        output_expr={
+            "residual1_u": lambda out: out["residual1_u"],
+            "residual2_u": lambda out: out["residual2_u"],
+            "residual3_u": lambda out: out["residual3_u"],
+        },
+        metric={"L2Rel": ppsci.metric.FunctionalMetric(eval_l2_rel_func)},
+        name="sup_validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # set optimizer
+    optimizer = ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(custom_model)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        custom_model,
+        constraint,
+        optimizer=optimizer,
+        validator=validator,
+        cfg=cfg,
+    )
+
+    solver.train()
+    solver.eval()
+
+    # visualize prediction
+    with solver.no_grad_context_manager(True):
+        for index, (_input, _label, _) in enumerate(sup_validator.data_loader):
+            u_exact = _label["residual_u_exact"]
+            output_ = custom_model(_input)
+            u_pred = paddle.concat(
+                [output_["residual1_u"], output_["residual2_u"], output_["residual3_u"]]
+            )
+
+            plotting.log_image(
+                residual1_x=_input["residual1_x"],
+                residual1_y=_input["residual1_y"],
+                residual2_x=_input["residual2_x"],
+                residual2_y=_input["residual2_y"],
+                residual3_x=_input["residual3_x"],
+                residual3_y=_input["residual3_y"],
+                interface1_x=_input["interface1_x"],
+                interface1_y=_input["interface1_y"],
+                interface2_x=_input["interface2_x"],
+                interface2_y=_input["interface2_y"],
+                boundary_x=_input["boundary_x"],
+                boundary_y=_input["boundary_y"],
+                residual_u_pred=u_pred,
+                residual_u_exact=u_exact,
+            )
+
+
+def evaluate(cfg: DictConfig):
+    layer_list = (
+        cfg.MODEL.layers1,
+        cfg.MODEL.layers2,
+        cfg.MODEL.layers3,
+    )
+
+    custom_model = model.Model(layer_list)
+
+    # set validator
+    eval_dataloader_cfg = {
+        "dataset": {
+            "name": "IterableMatDataset",
+            "file_path": cfg.DATA_FILE,
+            "input_keys": cfg.TRAIN.input_keys,
+            "label_keys": cfg.EVAL.label_keys,
+            "alias_dict": cfg.EVAL.alias_dict,
+        }
+    }
+
+    sup_validator = ppsci.validate.SupervisedValidator(
+        eval_dataloader_cfg,
+        loss=ppsci.loss.FunctionalLoss(loss_fun),
+        output_expr={
+            "residual1_u": lambda out: out["residual1_u"],
+            "residual2_u": lambda out: out["residual2_u"],
+            "residual3_u": lambda out: out["residual3_u"],
+        },
+        metric={"L2Rel": ppsci.metric.FunctionalMetric(eval_l2_rel_func)},
+        name="sup_validator",
+    )
+    validator = {sup_validator.name: sup_validator}
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        custom_model,
+        validator=validator,
+        cfg=cfg,
+    )
+
+    solver.eval()
+
+    # visualize prediction
+    with solver.no_grad_context_manager(True):
+        for index, (_input, _label, _) in enumerate(sup_validator.data_loader):
+            u_exact = _label["residual_u_exact"]
+            _output = custom_model(_input)
+            u_pred = paddle.concat(
+                [_output["residual1_u"], _output["residual2_u"], _output["residual3_u"]]
+            )
+
+            plotting.log_image(
+                residual1_x=_input["residual1_x"],
+                residual1_y=_input["residual1_y"],
+                residual2_x=_input["residual2_x"],
+                residual2_y=_input["residual2_y"],
+                residual3_x=_input["residual3_x"],
+                residual3_y=_input["residual3_y"],
+                interface1_x=_input["interface1_x"],
+                interface1_y=_input["interface1_y"],
+                interface2_x=_input["interface2_x"],
+                interface2_y=_input["interface2_y"],
+                boundary_x=_input["boundary_x"],
+                boundary_y=_input["boundary_y"],
+                residual_u_pred=u_pred,
+                residual_u_exact=u_exact,
+            )
+
+
+@hydra.main(version_base=None, config_path="./conf", config_name="xpinn.yaml")
+def main(cfg: DictConfig):
+    if cfg.mode == "train":
+        train(cfg)
+    elif cfg.mode == "eval":
+        evaluate(cfg)
+    else:
+        raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")
+
+
+if __name__ == "__main__":
+    main()
+
+

5. 结果展示

+

下方展示了对计算域中每个点的预测值结果、参考结果和相对误差。

+
+

+

+
预测结果和参考结果的对比
+
+

可以看到模型预测结果与真实结果相近,若增大训练轮数,模型精度会进一步提高。

+

6. 参考文献

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/install_setup/index.html b/zh/install_setup/index.html new file mode 100644 index 0000000000..c4e8c1e95a --- /dev/null +++ b/zh/install_setup/index.html @@ -0,0 +1,3699 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 安装使用 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

欢迎使用 PaddleScience

+

1. 开始安装

+

1.1 从 docker 镜像启动[可选]

+
+
+
+
# pull image
+docker pull hydrogensulfate/paddlescience
+
+# create a container named 'paddlescience_container' based on pulled image
+## docker version < 19.03
+nvidia-docker run --name paddlescience_container --network=host -it --shm-size 64g hydrogensulfate/paddlescience:latest /bin/bash
+
+## docker version >= 19.03
+# docker run --name paddlescience_container --gpus all --network=host -it shm-size 64g hydrogensulfate/paddlescience:latest /bin/bash
+
+
+

Note

+

Dockerhub 拉取的镜像预装了运行 PaddleScience 所需的依赖包,如 pymesh、open3d,并不包含 PaddleScience。 +因此请在镜像拉取和容器构建完成后,参考 1.4 安装 PaddleScience 中的步骤,在容器中安装 PaddleScience。

+
+
+
+
git clone https://github.com/PaddlePaddle/PaddleScience.git
+cd PaddleScience/docker/
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/docker/pymesh.tar.xz
+bash run.sh
+
+

如果出现因网络问题导致的 docker 构建时 apt 下载报错,则重复执行 bash run.sh 直至构建完成。

+

更多关于 Paddle Docker 的安装和使用,请参考 Docker 安装

+
+
+
+

1.2 python 环境安装[可选]

+

如果你还没有 python 环境或者 python 版本小于 3.9,则推荐使用 Anaconda 安装并配置 python 环境,否则可以忽略本步骤。

+
    +
  1. 根据系统环境,从 https://repo.anaconda.com/archive/ 中下载对应的 Anaconda3 安装包,并手动安装。
  2. +
  3. +

    创建 python 3.9 环境,并进入该环境。

    +
    # 使用 conda 创建 python 环境,并命名为 "ppsci_py39"
    +conda create -n ppsci_py39 python=3.9
    +
    +# 进入创建好的 "ppsci_py39" 环境
    +conda activate ppsci_py39
    +
    +
  4. +
+

1.3 安装 PaddlePaddle

+

请在 PaddlePaddle 官网按照您的运行环境,安装 develop 版的 PaddlePaddle。

+

安装完毕之后,运行以下命令,验证 Paddle 是否安装成功。

+
python -c "import paddle; paddle.utils.run_check()"
+
+

如果出现 PaddlePaddle is installed successfully! Let's start deep learning with PaddlePaddle now. 信息,说明您已成功安装,可以继续安装 PaddleScience。

+

1.4 安装 PaddleScience

+

1.4.1 安装基础功能

+

从以下三种安装方式中任选一种

+
+
+
+

执行以下命令,从 github 上 clone PaddleScience 源代码,并以 editable 的方式安装 PaddleScience。

+
git clone -b develop https://github.com/PaddlePaddle/PaddleScience.git
+# 若 github clone 速度比较慢,可以使用 gitee clone
+# git clone -b develop https://gitee.com/paddlepaddle/PaddleScience.git
+
+cd PaddleScience
+
+# install paddlesci with editable mode
+pip install -e . -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+
+
+

执行以下命令以 pip 的方式安装最新版本的 PaddleScience。

+
# nightly build
+pip install https://paddle-qa.bj.bcebos.com/PaddleScience/whl/latest/dist/paddlesci-0.0.0-py3-none-any.whl
+# release
+# pip install -U paddlesci
+
+
+
+

如果在您的环境中,上述两种方式都无法正常安装,则可以选择本方式,在终端内将环境变量 PYTHONPATH 临时设置为 PaddleScience绝对路径,如下所示。

+
+
+
+
cd PaddleScience/
+export PYTHONPATH=$PYTHONPATH:$PWD
+pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # manually install requirements
+
+
+
+
cd PaddleScience/
+set PYTHONPATH=%cd%
+pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # manually install requirements
+
+
+
+
+

上述方式的优点是步骤简单无需安装,缺点是当环境变量生效的终端被关闭后,需要重新执行上述命令设置 PYTHONPATH 才能再次使用 PaddleScience,较为繁琐。

+
+
+
+

1.4.2 安装额外功能[可选]

+

如需使用 .obj, .ply, .off, .stl, .mesh, .node, .poly and .msh 等复杂几何文件构建几何(计算域),以及使用加密采样等功能,则需按照下方给出的命令,安装 open3d、 +pybind11、pysdf、PyMesh 四个依赖库(上述1.1 从 docker 镜像启动中已安装上述依赖库)。

+

否则无法使用 ppsci.geometry.Mesh 等基于复杂几何文件的 API,因此也无法运行如 Aneurysm 等依赖 ppsci.geometry.Mesh API 的复杂案例。

+
+
+
+
pip install open3d -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+
+
+
pip install pybind11 -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+
+
+
pip install pysdf
+
+
+
+

在安装 PyMesh 之前,首先需通过 cmake --version 确认环境中是否已安装 cmake。 +如未安装,可按照下列命令下载、解压 cmake 包,并添加到 PATH 变量中即可完成安装。

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/cmake-3.23.0-linux-x86_64.tar.gz
+tar -zxvf cmake-3.23.0-linux-x86_64.tar.gz
+rm -f cmake-3.23.0-linux-x86_64.tar.gz
+PATH=$PWD/cmake-3.23.0-linux-x86_64/bin:$PATH
+
+# cmake --version
+# cmake version 3.23.0
+
+# CMake suite maintained and supported by Kitware (kitware.com/cmake).
+
+

PyMesh 库需要以 setup 的方式进行安装,命令如下:

+
wget -nc https://paddle-org.bj.bcebos.com/paddlescience/PyMesh.tar.gz
+tar -zxvf PyMesh.tar.gz
+
+# 也可以使用 git 命令下载,速度可能会比较慢
+# git clone https://github.com/PyMesh/PyMesh.git
+# git submodule update --init --recursive --progress
+
+cd PyMesh
+export PYMESH_PATH=`pwd`
+
+apt-get install \
+    libeigen3-dev \
+    libgmp-dev \
+    libgmpxx4ldbl \
+    libmpfr-dev \
+    libboost-dev \
+    libboost-thread-dev \
+    libtbb-dev \
+    python3-dev
+
+python -m pip install --user -r $PYMESH_PATH/python/requirements.txt
+python setup.py build
+python setup.py install --user
+
+# test whether installed successfully
+python -c "import pymesh; pymesh.test()"
+
+# Ran 175 tests in 3.150s
+
+# OK (SKIP=2)
+
+
+

安装注意事项

+

如果使用 git 命令下载 PyMesh 项目文件,则安装过程中可能会出现两个问题,可以按照以下方式解决:

+
    +
  1. +

    由于网络问题,git submodule update 过程中可能某些 submodule 会 clone 失败,此时只需 +反复执行 git submodule update --init --recursive --progress 直到所有库都 clone 成功即可。

    +
  2. +
  3. +

    所有 submodule 都 clone 成功后,请检查 PyMesh/third_party/ 下是否有空文件夹,若有则需 +手动找到并删除这些空文件夹,再执行 git submodule update --init --recursive --progress 命 +令即可恢复这些空文件夹至正常含有文件的状态,此时再继续执行剩余安装命令即可。

    +
  4. +
+
+
+
+
+

2. 验证安装

+
    +
  • +

    执行以下代码,验证安装的 PaddleScience 基础功能是否正常。

    +
    python -c "import ppsci; ppsci.run_check()"
    +
    +

    如果出现 PaddleScience is installed successfully.✨ 🍰 ✨,则说明安装验证成功。

    +
  • +
  • +

    [可选]如果已按照 1.4.2 安装额外依赖 正确安装了 4 个额外依赖库,则可以执行以下代码, + 验证 PaddleScience 的 ppsci.geometry.Mesh 模块是否能正常运行。

    +
    python -c "import ppsci; ppsci.run_check_mesh()"
    +
    +

    如果出现 ppsci.geometry.Mesh module running successfully.✨ 🍰 ✨,则说明该模块运行正常。

    +
  • +
+

3. 开始使用

+
    +
  • +

    运行内置的案例(以 ldc2d_unsteady_Re10.py 为例)

    +
    cd examples/ldc/
    +python ./ldc2d_unsteady_Re10.py
    +
    +
  • +
  • +

    编写自己的案例(假设案例名为 demo)

    +

    推荐在 examples/ 下新建 demo 文件夹,然后在 demo 文件夹下新建 demo.py,最后在 demo.py 文件中使用 PaddleScience 提供的 API 编写代码。

    +
    examples/demo/demo.py
    1
    +2
    +3
    import ppsci
    +
    +# write your code here...
    +
    +

    编写完毕后运行你的代码

    +
    cd examples/demo
    +python ./demo.py
    +
    +

    如不了解接下来该如何基于 PaddleScience 编写代码,则推荐参考 快速开始 和其他案例的文档、代码,进一步了解如何使用 ppsci 下的模块来编写自己的案例。

    +
  • +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/overview/index.html b/zh/overview/index.html new file mode 100644 index 0000000000..5d5f8a6b2c --- /dev/null +++ b/zh/overview/index.html @@ -0,0 +1,3723 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 功能介绍 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

PaddleScience 模块介绍

+

PaddleScience 在代码结构上划分为 12 个模块。从一般深度学习工作流的角度来看,这 12 个模块分别负责构建输入数据、构建神经网络模型、构建损失函数、构建优化器,训练、评估、可视化等功能。从科学计算角度来看,部分模块承担了不同于 CV、NLP 任务的功能,比如用于物理机理驱动的 Equation 模块,定义方程公式和辅助高阶微分计算;用于涉及几何场景采样的 Geometry 模块,定义简单、复杂几何形状并在其内部、边界采样构造数据;Constraint 模块将不同的优化目标视为一种“约束”,使得套件能用一套训练代码统一物理机理驱动、数据驱动、数理融合三种不同的求解流程。

+ +

panorama

+ + +

1. 整体工作流

+
+

workflow

+
+

上图是 PaddleScience 的 workflow 示意图(以基于几何的问题求解为例),流程描述如下

+
    +
  1. Geometry 负责构建几何并在几何上采样,完成数据构建;
  2. +
  3. 用 Model 模块接受输入,得到模型输出;
  4. +
  5. 科学计算任务具有特殊性,模型输出往往并不是前向计算的终点,还需要进一步按照 Equation,计算出方程公式所需的变量;
  6. +
  7. 计算损失函数,并利用框架的自动微分机制,求出所有参数的梯度;
  8. +
  9. 上述的优化目标可以施加在几何的不同区域上,比如interior、boundary区域,因此上图中的 Constraint 可以有多个;
  10. +
  11. 将所有 Constraint 贡献的梯度累加,并用于更新模型参数;
  12. +
  13. 训练过程中如果开启了评估和可视化功能,则会按一定频率自动对当前模型进行评估和预测结果可视化;
  14. +
  15. Solver 是整个套件运行的全局调度模块,负责将上述过程按用户指定的轮数和频率重复运行。
  16. +
+

2. 模块简介

+

2.1 Arch

+

Arch 模块负责各种神经网络模型的组网、参数初始化、前向计算等功能,内置了多种模型供用户使用。

+

2.2 AutoDiff

+

AutoDiff 模块负责计算高阶微分功能,内置基于 Paddle 自动微分机制的全局单例 jacobianhessian 供用户使用。

+

2.3 Constraint

+
+

constraint

+
+

为了在套件中统一物理信息驱动、数据驱动、数理融合三种求解方式,我们将数据构造、输入到输出的计算过程、损失函数等必要接口在其定义完毕之后,统一记录在 Constraint 这一模块中,有了这些接口,Constraint 就能表示不同的训练目标,如:

+
    +
  • InteriorConstraint 定义了在给定的几何区域内部,按照给定输入到输出的计算过程,利用损失函数优化模型参数,使得模型输出满足给定的条件;
  • +
  • BoundaryConstraint 定义了在给定的几何区域边界上,按照给定输入到输出的计算过程,利用损失函数优化模型参数,使得模型输出满足给定的条件;
  • +
  • SupervisedConstraint 定义了在给定的监督数据(相当于CV、NLP中的监督训练)上,按照给定输入到输出的计算过程,利用损失函数优化模型参数,使得模型输出满足给定的条件。
  • +
  • ...
  • +
+

这一模块有两个主要作用,一是在代码流程上统一了物理信息驱动、数据驱动两个不同的优化范式(前者类似监督训练方式,后者类似无监督训练方式),二是使得套件能应用在数理融合的场景中,只需分别构造不同的 Constraint 并让它们共同参与训练即可。

+

2.4 Data

+

Data 模块负责数据的读取、包装和预处理,如下所示。

+ + + + + + + + + + + + + + + + + + + + + +
子模块名称子模块功能
ppsci.data.dataset数据集相关
ppsci.data.transform单个数据样本预处理相关方法
ppsci.data.batch_transform批数据预处理相关方法
+

2.5 Equation

+
+

equation

+
+

Equation 模块负责定义各种常见方程的计算函数,如 NavierStokes 表示 N-S 方程,Vibration 表示振动方程,每个方程内部含有相关变量的计算函数。

+

2.6 Geometry

+
+

geometry

+
+

Geometry 模块负责定义各种常见的几何形状,如 Interval 线段几何、Rectangle 矩形几何、Sphere 球面几何。

+

2.7 Loss

+

Loss 模块包含 ppsci.loss.lossppsci.loss.mtl 两个子模块,如下所示。

+ + + + + + + + + + + + + + + + + +
子模块名称子模块功能
ppsci.loss.loss损失函数相关
ppsci.loss.mtl多目标优化相关
+

2.8 Optimizer

+

Optimizer 模块包含 ppsci.optimizer.optimizerppsci.optimizer.lr_scheduler 两个子模块,如下所示。

+ + + + + + + + + + + + + + + + + +
子模块名称子模块功能
ppsci.utils.optimizer优化器相关
ppsci.utils.lr_scheduler学习率调节器相关
+

2.9 Solver

+

Solver 模块负责定义求解器,作为训练、评估、推理、可视化的启动和管理引擎。

+

2.10 Utils

+

Utils 模块内部存放了一些适用于多种场景下的工具类、函数,例如在 reader.py 下的数据读取函数,在 logger.py 下的日志打印函数,以及在 expression.py 下的方程计算类。

+

根据其功能细分为以下 8 个子模块

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
子模块名称子模块功能
ppsci.utils.checkerppsci 安装功能检查相关
ppsci.utils.expression负责训练、评估、可视化过程中涉及模型、方程的前向计算
ppsci.utils.initializer常用参数初始化方法
ppsci.utils.logger日志打印模块
ppsci.utils.misc存放通用函数
ppsci.utils.reader文件读取模块
ppsci.utils.writer文件写入模块
ppsci.utils.save_load模型参数保存与加载
ppsci.utils.symbolicsympy 符号计算功能相关
+

2.11 Validate

+

Validator 模块负责定义各种评估器,用于在指定数据上进行评估(可选,默认不开启训练时评估),并得到评估指标。

+

2.12 Visualize

+

Visualizer 模块负责定义各种可视化器,用于模型评估完后在指定数据上进行预测(可选,默认不开启训练时可视化)并将结果保存成可视化的文件。

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/quickstart/index.html b/zh/quickstart/index.html new file mode 100644 index 0000000000..74d033b080 --- /dev/null +++ b/zh/quickstart/index.html @@ -0,0 +1,4170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 快速开始 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

快速开始

+

AI Studio快速体验

+

本文通过一个简单的 demo 及其扩展问题,介绍如何使用 PaddleScience 训练模型,解决一类方程学习与预测问题,并可视化预测结果。

+

1. 问题简介

+

假设我们希望用神经网络模型去拟合 \(x \in [-\pi, \pi]\) 区间内,\(u=\sin(x)\) 这一函数。在拟合函数已知和未知两种情形下,如何去尽可能地准确拟合 \(u=\sin(x)\)

+

第一种场景下,假设已知目标函数 \(u\) 的解析解就是 \(u=\sin(x)\),我们采用监督训练的思路,直接用该公式生成标签因变量 \(u\),与自变量 \(x\) 共同作为监督数据对模型进行训练。

+

第二种场景下,假设不知道目标函数 \(u\) 的解析解,但我们知道其满足某种微分关系,我们这里以其中一个满足条件的微分方程 \(\dfrac{\partial u} {\partial x}=\cos(x)\) 为例,介绍如何生成数据进行训练。

+

2. 场景一

+

目标拟合函数:

+
\[ +u=\sin(x), x \in [-\pi, \pi]. +\]
+

我们生成 \(N\) 组数据对 \((x_i, u_i), i=1,...,N\) 作为监督数据进行训练即可。

+

在撰写代码之前,我们首先导入必要的包。

+
1
+2
+3
+4
import numpy as np
+
+import ppsci
+from ppsci.utils import logger
+
+

然后创建日志和模型保存目录供训练过程记录和保存使用,这一步是绝大部分案例在正式开始前都需要进行的操作。

+
 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
# set random seed(42) for reproducibility
+ppsci.utils.misc.set_random_seed(42)
+
+# set output directory
+OUTPUT_DIR = "./output_quick_start_case1"
+
+# initialize logger while create output directory
+logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+

接下来正式开始撰写代码。

+

首先定义问题区间,我们使用 ppsci.geometry.Interval 定义一个线段几何形状,方便后续在该线段上对 \(x\) 进行采样。

+
15
+16
+17
# set 1D-geometry domain([-π, π])
+l_limit, r_limit = -np.pi, np.pi
+x_domain = ppsci.geometry.Interval(l_limit, r_limit)
+
+

然后定义一个简单的 3 层 MLP 模型。

+
# set model to 3-layer MLP
+model = ppsci.arch.MLP(("x",), ("u",), 3, 64)
+
+

上述代码表示模型接受自变量 \(x\) 作为输入,输出预测结果 \(\hat{u}\)

+

然后我们定义已知的 \(u=\sin(x)\) 计算函数,作为 ppsci.constraint.InteriorConstraint 的参数,用于计算标签数据,InteriorConstraint 表示以给定的几何形状或数据集中的数据作为输入,联合给定的标签数据,指导模型进行优化。

+
22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
# standard solution of sin(x)
+def sin_compute_func(data: dict):
+    return np.sin(data["x"])
+
+
+# set constraint on 1D-geometry([-π, π])
+ITERS_PER_EPOCH = 100  # use 100 iterations per training epoch
+interior_constraint = ppsci.constraint.InteriorConstraint(
+    output_expr={"u": lambda out: out["u"]},
+    label_dict={"u": sin_compute_func},
+    geom=x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 32,  # use 32 samples(points) per iteration for interior constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+)
+# wrap constraint(s) into one dict
+constraint = {
+    interior_constraint.name: interior_constraint,
+}
+
+

此处的 interior_constraint 表示一个训练目标,即我们希望在 \([-\pi, \pi]\) 这段区间内,优化模型让模型的预测结果 \(\hat{u}\) 尽可能地接近它的标签值 \(u\)

+

接下来就可以开始定义模型训练相关的内容,比如训练轮数、优化器、可视化器。

+
48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
# set training hyper-parameters
+EPOCHS = 10
+# set optimizer
+optimizer = ppsci.optimizer.Adam(2e-3)(model)
+
+# set visualizer
+visual_input_dict = {
+    "x": np.linspace(l_limit, r_limit, 1000, dtype="float32").reshape(1000, 1)
+}
+visual_input_dict["u_ref"] = np.sin(visual_input_dict["x"])
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visual_input_dict,
+        ("x",),
+        {"u_pred": lambda out: out["u"], "u_ref": lambda out: out["u_ref"]},
+        prefix="u=sin(x)",
+    ),
+}
+
+

最后将上述定义的对象传递给训练调度类 Solver,即可开始模型训练

+
67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer,
+    epochs=EPOCHS,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+
+

训练完毕后再用刚才取的 1000 个点与标准解计算 L2-相对误差

+
81
+82
+83
+84
+85
+86
# compute l2-relative error of trained model
+pred_u = solver.predict(visual_input_dict, return_numpy=True)["u"]
+l2_rel = np.linalg.norm(pred_u - visual_input_dict["u_ref"]) / np.linalg.norm(
+    visual_input_dict["u_ref"]
+)
+logger.info(f"l2_rel = {l2_rel:.5f}")
+
+

再对这 1000 个点的预测结果进行可视化

+
# visualize prediction after finished training
+solver.visualize()
+
+

训练记录下所示

+
...
+...
+ppsci INFO: [Train][Epoch  9/10][Iter  80/100] lr: 0.00200, loss: 0.00663, EQ: 0.00663, batch_cost: 0.00180s, reader_cost: 0.00011s, ips: 17756.64, eta: 0:00:00
+ppsci INFO: [Train][Epoch  9/10][Iter  90/100] lr: 0.00200, loss: 0.00598, EQ: 0.00598, batch_cost: 0.00180s, reader_cost: 0.00011s, ips: 17793.97, eta: 0:00:00
+ppsci INFO: [Train][Epoch  9/10][Iter 100/100] lr: 0.00200, loss: 0.00547, EQ: 0.00547, batch_cost: 0.00179s, reader_cost: 0.00011s, ips: 17864.08, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  10/100] lr: 0.00200, loss: 0.00079, EQ: 0.00079, batch_cost: 0.00182s, reader_cost: 0.00012s, ips: 17547.05, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  20/100] lr: 0.00200, loss: 0.00075, EQ: 0.00075, batch_cost: 0.00183s, reader_cost: 0.00011s, ips: 17482.92, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  30/100] lr: 0.00200, loss: 0.00077, EQ: 0.00077, batch_cost: 0.00182s, reader_cost: 0.00011s, ips: 17539.51, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  40/100] lr: 0.00200, loss: 0.00074, EQ: 0.00074, batch_cost: 0.00182s, reader_cost: 0.00011s, ips: 17587.51, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  50/100] lr: 0.00200, loss: 0.00071, EQ: 0.00071, batch_cost: 0.00182s, reader_cost: 0.00011s, ips: 17563.59, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  60/100] lr: 0.00200, loss: 0.00070, EQ: 0.00070, batch_cost: 0.00182s, reader_cost: 0.00011s, ips: 17604.60, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  70/100] lr: 0.00200, loss: 0.00074, EQ: 0.00074, batch_cost: 0.00181s, reader_cost: 0.00011s, ips: 17699.28, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  80/100] lr: 0.00200, loss: 0.00077, EQ: 0.00077, batch_cost: 0.00180s, reader_cost: 0.00011s, ips: 17764.92, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  90/100] lr: 0.00200, loss: 0.00075, EQ: 0.00075, batch_cost: 0.00180s, reader_cost: 0.00011s, ips: 17795.87, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter 100/100] lr: 0.00200, loss: 0.00071, EQ: 0.00071, batch_cost: 0.00179s, reader_cost: 0.00011s, ips: 17872.00, eta: 0:00:00
+
+

训练完毕后再用刚才取的 1000 个点与标准解计算 L2-相对误差

+
81
+82
+83
+84
+85
+86
# compute l2-relative error of trained model
+pred_u = solver.predict(visual_input_dict, return_numpy=True)["u"]
+l2_rel = np.linalg.norm(pred_u - visual_input_dict["u_ref"]) / np.linalg.norm(
+    visual_input_dict["u_ref"]
+)
+logger.info(f"l2_rel = {l2_rel:.5f}")
+
+

可以看到利用标准解监督训练模型,在标准解附近仍有很好的预测能力,L2-相对误差为 0.02677。

+

预测结果可视化如下所示

+

u=sin(x) prediction

+

场景一的完整代码如下所示

+
examples/quick_start/case1.py
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
import numpy as np
+
+import ppsci
+from ppsci.utils import logger
+
+# set random seed(42) for reproducibility
+ppsci.utils.misc.set_random_seed(42)
+
+# set output directory
+OUTPUT_DIR = "./output_quick_start_case1"
+
+# initialize logger while create output directory
+logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+# set 1D-geometry domain([-π, π])
+l_limit, r_limit = -np.pi, np.pi
+x_domain = ppsci.geometry.Interval(l_limit, r_limit)
+
+# set model to 3-layer MLP
+model = ppsci.arch.MLP(("x",), ("u",), 3, 64)
+
+# standard solution of sin(x)
+def sin_compute_func(data: dict):
+    return np.sin(data["x"])
+
+
+# set constraint on 1D-geometry([-π, π])
+ITERS_PER_EPOCH = 100  # use 100 iterations per training epoch
+interior_constraint = ppsci.constraint.InteriorConstraint(
+    output_expr={"u": lambda out: out["u"]},
+    label_dict={"u": sin_compute_func},
+    geom=x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 32,  # use 32 samples(points) per iteration for interior constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+)
+# wrap constraint(s) into one dict
+constraint = {
+    interior_constraint.name: interior_constraint,
+}
+
+# set training hyper-parameters
+EPOCHS = 10
+# set optimizer
+optimizer = ppsci.optimizer.Adam(2e-3)(model)
+
+# set visualizer
+visual_input_dict = {
+    "x": np.linspace(l_limit, r_limit, 1000, dtype="float32").reshape(1000, 1)
+}
+visual_input_dict["u_ref"] = np.sin(visual_input_dict["x"])
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visual_input_dict,
+        ("x",),
+        {"u_pred": lambda out: out["u"], "u_ref": lambda out: out["u_ref"]},
+        prefix="u=sin(x)",
+    ),
+}
+
+# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer,
+    epochs=EPOCHS,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+
+# compute l2-relative error of trained model
+pred_u = solver.predict(visual_input_dict, return_numpy=True)["u"]
+l2_rel = np.linalg.norm(pred_u - visual_input_dict["u_ref"]) / np.linalg.norm(
+    visual_input_dict["u_ref"]
+)
+logger.info(f"l2_rel = {l2_rel:.5f}")
+
+# visualize prediction after finished training
+solver.visualize()
+
+

3. 场景二

+

可以看到场景一的监督训练方式能较好地解决函数拟合问题,但一般情况下我们是无法得知拟合函数本身的解析式的,因此也无法直接构造因变量的监督数据。

+

虽然无法求出解析式直接构造监督数据,但往往可以利用相关数学知识,推导出目标拟合函数符合的某种数学关系,以训练模型以满足这种数学关系的方式,达到以“间接监督”的方式优化模型的目的。

+

假设我们不再使用 \(u=\sin(x)\) 这一先验公式,因而无法计算标签数据 \(u\)。因此使用如下方程组,其含有一个偏微分方程和边界条件

+
\[ +\begin{cases} +\begin{aligned} + \dfrac{\partial u} {\partial x} &= \cos(x) \\ + u(-\pi) &= 2 +\end{aligned} +\end{cases} +\]
+

构造数据对 \((x_i, \cos(x_i)), i=1,...,N\)。 +这意味着我们仍然能保持模型的输入、输出不变,但优化目标变成了:让 \(\dfrac{\partial \hat{u}} {\partial x}\) 尽可能地接近 \(\cos(x)\),且 \(\hat{u}(-\pi)\) 也要尽可能地接近 \(2\)

+

基于以上理论,我们对场景一的代码进行少量的改写即可得到本场景二的代码。

+

首先由于我们需要使用一阶微分这一操作,因此在代码开头处需导入一阶微分 API

+
1
+2
+3
+4
+5
import numpy as np
+
+import ppsci
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+

然后在原来的标签计算函数下方,新增一个微分标签值计算函数

+
28
+29
+30
# standard solution of cos(x)
+def cos_compute_func(data: dict):
+    return np.cos(data["x"])
+
+

接着将 interior_constraint 这一约束条件从约束“模型输出”,改为约束“模型输出对输入的一阶微分”

+
33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
# set constraint on 1D-geometry([-π, π])
+ITERS_PER_EPOCH = 100  # use 100 iterations per training epoch
+interior_constraint = ppsci.constraint.InteriorConstraint(
+    output_expr={"du_dx": lambda out: jacobian(out["u"], out["x"])},
+    label_dict={"du_dx": cos_compute_func},
+    geom=x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 32,  # use 32 samples(points) per iteration for interior constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+)
+
+

考虑到一般情况下偏微分方程的解会存在待定系数,需通过定解条件(初(边)值条件)来确定,因此需要在 interior_constraint 构建代码的后面,额外添加一个边界条件约束 bc_constraint,如下所示

+
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
bc_constraint = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda d: d["u"]},
+    {"u": lambda d: sin_compute_func(d) + 2},  # (1)
+    x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 1,  # use 1 sample(point) per iteration for boundary constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+    criteria=lambda x: np.isclose(x, l_limit),
+)
+
+
    +
  1. 对应边界条件 \(u(x_0)=sin(x_0)+2\)
  2. +
+

然后将该边界约束 bc_constraint 添加到 constraint

+
66
+67
+68
+69
+70
# wrap constraint(s) into one dict
+constraint = {
+    interior_constraint.name: interior_constraint,
+    bc_constraint.name: bc_constraint,
+}
+
+

同样地,修改 Visualizer 绘制的标准解为 \(sin(x)+2\)

+
77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
# set visualizer
+visual_input_dict = {
+    "x": np.linspace(l_limit, r_limit, 1000, dtype="float32").reshape(1000, 1)
+}
+visual_input_dict["u_ref"] = np.sin(visual_input_dict["x"]) + 2.0
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visual_input_dict,
+        ("x",),
+        {"u_pred": lambda out: out["u"], "u_ref": lambda out: out["u_ref"]},
+        prefix="u=sin(x)",
+    ),
+}
+
+

修改完毕后执行训练

+
# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer,
+    epochs=EPOCHS,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+
+

训练日志如下所示

+
...
+...
+ppsci INFO: [Train][Epoch  9/10][Iter  90/100] lr: 0.00200, loss: 0.00176, EQ: 0.00087, BC: 0.00088, batch_cost: 0.00346s, reader_cost: 0.00024s, ips: 9527.80, eta: 0:00:00
+ppsci INFO: [Train][Epoch  9/10][Iter 100/100] lr: 0.00200, loss: 0.00170, EQ: 0.00087, BC: 0.00083, batch_cost: 0.00349s, reader_cost: 0.00024s, ips: 9452.07, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  10/100] lr: 0.00200, loss: 0.00107, EQ: 0.00072, BC: 0.00035, batch_cost: 0.00350s, reader_cost: 0.00025s, ips: 9424.75, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  20/100] lr: 0.00200, loss: 0.00116, EQ: 0.00083, BC: 0.00033, batch_cost: 0.00350s, reader_cost: 0.00025s, ips: 9441.33, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  30/100] lr: 0.00200, loss: 0.00103, EQ: 0.00079, BC: 0.00024, batch_cost: 0.00355s, reader_cost: 0.00025s, ips: 9291.90, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  40/100] lr: 0.00200, loss: 0.00108, EQ: 0.00078, BC: 0.00030, batch_cost: 0.00353s, reader_cost: 0.00025s, ips: 9348.09, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  50/100] lr: 0.00200, loss: 0.00163, EQ: 0.00082, BC: 0.00082, batch_cost: 0.00350s, reader_cost: 0.00024s, ips: 9416.24, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  60/100] lr: 0.00200, loss: 0.00160, EQ: 0.00083, BC: 0.00077, batch_cost: 0.00353s, reader_cost: 0.00024s, ips: 9345.73, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  70/100] lr: 0.00200, loss: 0.00150, EQ: 0.00082, BC: 0.00068, batch_cost: 0.00351s, reader_cost: 0.00024s, ips: 9393.89, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  80/100] lr: 0.00200, loss: 0.00146, EQ: 0.00081, BC: 0.00064, batch_cost: 0.00350s, reader_cost: 0.00024s, ips: 9424.81, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter  90/100] lr: 0.00200, loss: 0.00138, EQ: 0.00081, BC: 0.00058, batch_cost: 0.00349s, reader_cost: 0.00024s, ips: 9444.12, eta: 0:00:00
+ppsci INFO: [Train][Epoch 10/10][Iter 100/100] lr: 0.00200, loss: 0.00133, EQ: 0.00079, BC: 0.00054, batch_cost: 0.00349s, reader_cost: 0.00024s, ips: 9461.54, eta: 0:00:00
+
+

训练完毕后再用刚才取的 1000 个点与标准解计算 L2-相对误差

+
# compute l2-relative error of trained model
+pred_u = solver.predict(visual_input_dict, return_numpy=True)["u"]
+l2_rel = np.linalg.norm(pred_u - visual_input_dict["u_ref"]) / np.linalg.norm(
+    visual_input_dict["u_ref"]
+)
+logger.info(f"l2_rel = {l2_rel:.5f}")
+
+

可以看到利用微分方程训练的模型,在标准解附近仍有很好的预测能力,L2-相对误差为 0.00564。

+

预测结果可视化如下所示

+

u=sin(x)+2 prediction

+

可以发现利用微分关系训练的模型仍然具备良好的预测能力,并且结合定解条件,能学习出同时符合微分方程和定解条件的正确解模型。

+

场景二的完整代码如下所示

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
import numpy as np
+
+import ppsci
+from ppsci.autodiff import jacobian
+from ppsci.utils import logger
+
+# set random seed(42) for reproducibility
+ppsci.utils.misc.set_random_seed(42)
+
+# set output directory
+OUTPUT_DIR = "./output_quick_start_case2"
+
+# initialize logger while create output directory
+logger.init_logger("ppsci", f"{OUTPUT_DIR}/train.log", "info")
+
+# set 1D-geometry domain([-π, π])
+l_limit, r_limit = -np.pi, np.pi
+x_domain = ppsci.geometry.Interval(l_limit, r_limit)
+
+# set model to 3-layer MLP
+model = ppsci.arch.MLP(("x",), ("u",), 3, 64)
+
+# standard solution of sin(x)
+def sin_compute_func(data: dict):
+    return np.sin(data["x"])
+
+
+# standard solution of cos(x)
+def cos_compute_func(data: dict):
+    return np.cos(data["x"])
+
+
+# set constraint on 1D-geometry([-π, π])
+ITERS_PER_EPOCH = 100  # use 100 iterations per training epoch
+interior_constraint = ppsci.constraint.InteriorConstraint(
+    output_expr={"du_dx": lambda out: jacobian(out["u"], out["x"])},
+    label_dict={"du_dx": cos_compute_func},
+    geom=x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 32,  # use 32 samples(points) per iteration for interior constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+)
+bc_constraint = ppsci.constraint.BoundaryConstraint(
+    {"u": lambda d: d["u"]},
+    {"u": lambda d: sin_compute_func(d) + 2},  # (1)
+    x_domain,
+    dataloader_cfg={
+        "dataset": "NamedArrayDataset",
+        "iters_per_epoch": ITERS_PER_EPOCH,
+        "sampler": {
+            "name": "BatchSampler",
+            "shuffle": True,
+        },
+        "batch_size": 1,  # use 1 sample(point) per iteration for boundary constraint
+    },
+    loss=ppsci.loss.MSELoss(),
+    criteria=lambda x: np.isclose(x, l_limit),
+)
+# wrap constraint(s) into one dict
+constraint = {
+    interior_constraint.name: interior_constraint,
+    bc_constraint.name: bc_constraint,
+}
+
+# set training hyper-parameters
+EPOCHS = 10
+# set optimizer
+optimizer = ppsci.optimizer.Adam(2e-3)(model)
+
+# set visualizer
+visual_input_dict = {
+    "x": np.linspace(l_limit, r_limit, 1000, dtype="float32").reshape(1000, 1)
+}
+visual_input_dict["u_ref"] = np.sin(visual_input_dict["x"]) + 2.0
+visualizer = {
+    "visualize_u": ppsci.visualize.VisualizerScatter1D(
+        visual_input_dict,
+        ("x",),
+        {"u_pred": lambda out: out["u"], "u_ref": lambda out: out["u_ref"]},
+        prefix="u=sin(x)",
+    ),
+}
+
+# initialize solver
+solver = ppsci.solver.Solver(
+    model,
+    constraint,
+    OUTPUT_DIR,
+    optimizer,
+    epochs=EPOCHS,
+    iters_per_epoch=ITERS_PER_EPOCH,
+    visualizer=visualizer,
+)
+# train model
+solver.train()
+
+# compute l2-relative error of trained model
+pred_u = solver.predict(visual_input_dict, return_numpy=True)["u"]
+l2_rel = np.linalg.norm(pred_u - visual_input_dict["u_ref"]) / np.linalg.norm(
+    visual_input_dict["u_ref"]
+)
+logger.info(f"l2_rel = {l2_rel:.5f}")
+
+# visualize prediction after finished training
+solver.visualize()
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/reproduction/index.html b/zh/reproduction/index.html new file mode 100644 index 0000000000..36ea06d918 --- /dev/null +++ b/zh/reproduction/index.html @@ -0,0 +1,3758 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 复现指南 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

模型复现流程及验收标准

+

本文档介绍如何基于 PaddleScience 套件进行模型复现并最终贡献到 PaddleScience 套件中

+

1. 背景说明

+

1.1 复现目标

+

在 AI for Science (以下简称 AI4S) 领域的模型复现任务中,通常需要对标准论文中的原始代码基于飞桨进行复现,同时也可能会涉及一些开放性的任务,如会提供建设目标,选手自行使用飞桨进行全部代码的创建。围绕以上任务,制定如下模型复现流程、验收标准与方法,其中每个阶段的每个过程均需要形成对应的说明文档

+

1.2 飞桨与 PaddleScience

+

除特殊说明外,模型复现默认要求基于 PaddleScience

+
    +
  • +

    PaddleScience

    +

    基于飞桨的 AI4S 套件,提供面向 AI4S 领域通用功能,如复杂几何形状解析、通用微分方程、数据驱动/物理机理/数理融合等求解器,方便开发 AI4S 领域相关模型,具体参考 PaddleScience 文档

    +
  • +
  • +

    飞桨PaddlePaddle

    +

    国产开源深度学习平台,提供面向深度学习的基础功能与通用API,方便开发深度学习模型,具体参考 Paddle API 文档

    +
  • +
+

2. 复现步骤

+

优先基于 PaddleScience 复现,如复现代码的实现逻辑和 PaddleScience 存在严重冲突,可考虑使用 PaddlePaddle API 复现。

+

reproduce process

+

2.1 基准论文理解、原始代码跑通、论文结果复现

+

该阶段需选手认真理解原始论文与提供的代码,并按照背景、方法与模型、训练与验证、结果、展望与致谢等分章节说明论文。过程中需要验证论文提供的代码,确认代码完整且可执行,且能够得到论文中列出的结果。 *此部分主要目的是为了其他用户更好的理解该复现工作的意义- 。关键示意如下:

+
    +
  1. +

    原始代码验证

    +

    初步对照基准论文中提供的结果,评估提供的代码是否完整、可执行,具体为:

    +
      +
    • 若有问题,则需要与飞桨团队反馈论文问题,由飞桨团队评估论文/模型复现的必要性,若该论文/模型价值较高,且具备了复现的条件后,则可重新进入复现流程;
    • +
    • 若无问题,则正式进入论文/模型的复现阶段
    • +
    +
  2. +
  3. +

    背景说明

    +

    结合给定的论文,需解释论文描述的领域问题(eg. 流体、材料等)、介绍自己的复现/开发工作,即对基准论文中摘要、简介有完整的介绍(开放课题则描述该课题的背景),之后对论文的方法,应用的模型进行说明。

    +
  4. +
  5. +

    方法与模型

    +

    解释基准论文所用到的理论、方法以及所用网络模型的原理等,即对论文的核心技术部分进行说明,同时说明论文中该方法、模型所体现的价值(或相对于传统求解方法的优势等)。

    +
  6. +
  7. +

    训练与验证

    +

    对基准论文中提供的典型案例,整体说明案例的原理(eg. 流体 N-S 方程求解)、说明训练的过程(如数据集来源、内容,超参数设定,硬件环境及训练时间等)。

    +
  8. +
+

2.2 基于 PaddleScience 的代码复现

+
    +
  1. +

    如果源代码使用 pytorch 实现,可优先使用 PaConvert 一键自动转换 pytorch 代码为 paddle 代码,配合 PaDiff 验证转换前后模型的精度对齐情况;如果源代码使用非 pytorch 实现(如 tensorflow),则需要列出需要基于飞桨复现的 API,同时需要形成 API 映射表,基于该映射表手动转换 tensorflow API 为 paddle API,若飞桨缺少对应的API可着重说明。

    +
  2. +
  3. +

    飞桨代码跑通

    +

    完成基于 API 复现后,需要结合论文中的模型,实现基于飞桨的代码跑通,并针对前反向使能能对齐使用 PaDiff 工具进行迭代、修正(如可能需要与原始代码逐步对齐)。

    +
  4. +
  5. +

    飞桨代码转换成 PaddleScience 代码

    +

    提高代码复用性和简洁性,需要将 Paddle 实现转换成 PaddleScience 套件提供的 API 实现,PaddleScience 提供了面向 AI4S 领域通用功能。具体流程请参考:PaddleScience 开发指南

    +
  6. +
  7. +

    复现论文中指标结果

    +

    结合论文中提到的典型 demo,进行关键指标复现,即能够基于飞桨复现论文中的指标,如图表、数据等,其收敛趋势、数值等应基本一致(与作者提供指标的相对误差在 ±10% 以内即可)

    +
  8. +
  9. +

    代码合入,完成复现

    +

    若以上活动均完全达成,则可以将论文/模型复现的代码按照下文中“代码规范”的要求,提交 PR 合入到指定的 Repo,通过评审后即可认为完成 90% 的复现工作,并可进行验收。

    +
  10. +
+

3 验收标准

+

3.1 交付产物列表

+ + + + + + + + + + + + + + + + + +
产物具体内容
模型基于 PaddleScience 复现的代码、模型预训练参数
文档AIStudio 文档、PaddleScience 文档
+

3.2 具体评估标准

+

3.2.1 模型正确性评估

+

定性分析:模型收敛曲线、最终效果图示与论文能够保持一致 +定量分析:如果论文包含定量指标,则复现指标与论文指标相对误差需满足: \(\dfrac{|复现指标-源代码指标|}{源代码指标} \le 10\%\)

+

3.2.2 代码规范

+

整体代码规范遵循 PEP8 https://peps.python.org/pep-0008/ ,除此之外需要注意:

+
    +
  • 文件和文件夹命名中,尽量使用下划线 _ 代表空格,不要使用 -
  • +
  • 模型定义过程中,需要有一个统一的变量(parameter)命名管理手段,如尽量手动声明每个变量的名字并支持名称可变,禁止将名称定义为一个常数(如 "embedding"),避免在复用代码阶段出现各种诡异的问题。
  • +
  • 重要文件,变量的名称定义过程中需要能够通过名字表明含义,禁止使用含混不清的名称,如 net.py, aaa.py 等。
  • +
  • 在代码中定义文件(夹)路径时,需要使用 os.path.join 完成,禁止使用 string 相加的方式,这会导致模型对 windows 环境缺乏支持。
  • +
  • +

    对于代码中重要的部分,需要加入注释介绍功能,帮助用户快速熟悉代码结构,包括但不仅限于:

    +
  • +
  • +

    Dataset、DataLoader的定义。

    +
  • +
  • 整个模型定义,包括input,运算过程,loss等内容。
  • +
  • init,save,load,等io部分。
  • +
  • 运行中间的关键状态,如print loss,save model等。
  • +
  • +

    一个比较符合代码规范的例子如下。

    +
    from paddle import io
    +from paddle.vision import transforms as T
    +from PIL import Image
    +import numpy as np
    +
    +
    +IMAGE_SIZE = 256
    +
    +
    +class PetDataset(io.Dataset):
    +    """
    +    Pet 数据集定义
    +    """
    +
    +    def __init__(self, mode="train"):
    +        """
    +        构造函数
    +        """
    +        if mode not in ["train", "test", "predict"]:
    +            raise ValueError(
    +                f"mode should be 'train' or 'test' or 'predict', but got {mode}"
    +            )
    +        self.image_size = IMAGE_SIZE
    +        self.mode = mode
    +        self.train_images = []
    +        self.label_images = []
    +        with open(f"./{self.mode}.txt", "r") as f:
    +            for line in f.readlines():
    +                image, label = line.strip().split("\t")
    +                self.train_images.append(image)
    +                self.label_images.append(label)
    +
    +    def _load_img(self, path, color_mode="rgb", transforms=[]):
    +        """
    +        统一的图像处理接口封装,用于规整图像大小和通道
    +        """
    +        img = Image.open(path)
    +        if color_mode == "grayscale":
    +            # if image is not already an 8-bit, 16-bit or 32-bit grayscale image
    +            # convert it to an 8-bit grayscale image.
    +            if img.mode not in ("L", "I;16", "I"):
    +                img = img.convert("L")
    +        elif color_mode == "rgba":
    +            if img.mode != "RGBA":
    +                img = img.convert("RGBA")
    +        elif color_mode == "rgb":
    +            if img.mode != "RGB":
    +                img = img.convert("RGB")
    +        else:
    +            raise ValueError(
    +                f"color_mode should be 'grayscale', 'rgb', or 'rgba', but got {color_mode}"
    +            )
    +        return T.Compose([T.Resize(self.image_size)] + transforms)(img)
    +
    +    def __getitem__(self, idx):
    +        """
    +        返回 image, label
    +        """
    +        train_image = self._load_img(
    +            self.train_images[idx],
    +            transforms=[T.Transpose(), T.Normalize(mean=127.5, std=127.5)],
    +        )  # 加载原始图像
    +        label_image = self._load_img(
    +            self.label_images[idx], color_mode="grayscale", transforms=[T.Grayscale()]
    +        )  # 加载Label图像
    +        # 返回image, label
    +        train_image = np.array(train_image, dtype="float32")
    +        label_image = np.array(label_image, dtype="int64")
    +        return train_image, label_image
    +
    +    def __len__(self):
    +        """
    +        返回数据集总数
    +        """
    +        return len(self.train_images)
    +
    +
  • +
  • +

    提供的代码能正常跑通训练、评估。

    +
  • +
+

3.2.3 文档规范

+

AIStudio 文档参考:PaddleScience-DarcyFlow - 飞桨AI Studio

+

PaddleScience 官网文档需满足:

+
    +
  • 复现完成后需撰写模型文档,需包含模型简介、问题定义、问题求解(逐步讲解训练评估以及可视化代码的编写过程)、完整代码、结果展示图等章节。
  • +
  • 在文档的开始,需要添加复现的论文题目、论文地址以及参考代码的链接,同时建议对参考代码的作者表示感谢。
  • +
  • 代码封装得当,易读性好,不用一些随意的变量/类/函数命名。
  • +
  • 注释清晰,不仅说明做了什么,也要说明为什么这么做。
  • +
  • 如果模型依赖 PaddlePaddle 未涵盖的依赖(如 pandas),则需要在文档开头对说明需安装哪些依赖项。
  • +
  • 随机控制,需要尽量固定含有随机因素模块的随机种子,保证模型可以正常复现(PaddleScience 套件提供了 ppsci.utils.misc.set_random_seed(seed_num) 语句来控制全局随机数)。
  • +
  • 超参数:模型内部超参数禁止写死,尽量都可以通过配置文件进行配置。
  • +
  • 文档末尾附上参考论文、参考代码网址、复现训练好的模型参数下载链接。 整体文档撰写可以参考:文档参考样例(darcy2d)
  • +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/technical_doc/index.html b/zh/technical_doc/index.html new file mode 100644 index 0000000000..825125e87f --- /dev/null +++ b/zh/technical_doc/index.html @@ -0,0 +1,3251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 技术稿件 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

技术稿件

+

详情请查看微信公众号 AI for Science

+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/tutorials/index.html b/zh/tutorials/index.html new file mode 100644 index 0000000000..ebe79dcc60 --- /dev/null +++ b/zh/tutorials/index.html @@ -0,0 +1,3316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 学习资料 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zh/user_guide/index.html b/zh/user_guide/index.html new file mode 100644 index 0000000000..cc2a9492ad --- /dev/null +++ b/zh/user_guide/index.html @@ -0,0 +1,4835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用指南 - PaddleScience Docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 跳转至 + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

使用指南

+

本文档介绍如何使用 PaddleScience 中的常用基础功能和进阶功能,基础功能包括断点继续训练、迁移学习、模型评估、模型推理;进阶功能包括分布式训练(暂时只支持数据并行)、混合精度训练、梯度累加。

+

1. 基础功能

+

1.1 使用 YAML + hydra

+

PaddleScience 推荐使用 YAML 文件控制程序训练、评估、推理等过程。其主要原理是利用 hydra 配置管理工具,从 *.yaml 格式的文件中解析配置参数,并传递给运行代码,以对程序运行时所使用的超参数等字段进行灵活配置,提高实验效率。本章节主要介绍 hydra 配置管理工具的基本使用方法。

+

在使用 hydra 配置运行参数前,请先执行以下命令检查是否已安装 hydra

+
pip show hydra-core
+
+

如未安装,则需执行以下命令安装 hydra

+
pip install hydra-core
+
+

1.1.1 打印运行配置

+
+

Warning

+

注意本教程内的打印运行配置方法只作为调试使用,hydra 默认在打印完配置后会立即结束程序。因此在正常运行程序时请勿加上 -c job 参数。

+
+

以 bracket 案例为例,其正常运行命令为:python bracket.py。若在其运行命令末尾加上 -c job,则可以打印出从运行配置文件 conf/bracket.yaml 中解析出的配置参数,如下所示。

+
$ python bracket.py -c job
mode: train
+seed: 2023
+output_dir: ${hydra:run.dir}
+log_freq: 20
+NU: 0.3
+E: 100000000000.0
+...
+...
+EVAL:
+  pretrained_model_path: null
+  eval_during_train: true
+  eval_with_no_grad: true
+  batch_size:
+    sup_validator: 128
+
+

1.1.2 命令行方式配置参数

+

仍然以配置文件 bracket.yaml 为例,关于学习率部分的参数配置如下所示。

+
bracket.yaml
...
+TRAIN:
+  epochs: 2000
+  iters_per_epoch: 1000
+  save_freq: 20
+  eval_during_train: true
+  eval_freq: 20
+  lr_scheduler:
+    epochs: ${TRAIN.epochs} # (1)
+    iters_per_epoch: ${TRAIN.iters_per_epoch}
+    learning_rate: 0.001
+    gamma: 0.95
+    decay_steps: 15000
+    by_epoch: false
+...
+
+
    +
  1. ${...}$ 是 omegaconf 的引用语法,可以引用配置文件中其他位置上的参数,避免同时维护多个相同语义的参数副本,其效果与 yaml 的 anchor 语法类似。
  2. +
+

可以看到上述配置文件中的学习率为 0.001,若需修改学习率为 0.002 以运行新的实验,则有以下两种方式:

+
    +
  • 将上述配置文件中的 learning_rate: 0.001 改为 learning_rate: 0.002,然后再运行程序。这种方式虽然简单,但在实验较多时容易造成实验混乱,因此不推荐使用。
  • +
  • +

    通过命令行参数的方式进行修改,如下所示。

    +
    python bracket.py TRAIN.lr_scheduler.learning_rate=0.002
    +
    +

    这种方式通过命令行参数临时重载运行配置,而不会对 bracket.yaml 文件本身进行修改,能灵活地控制运行时的配置,保证不同实验之间互不干扰。

    +
  • +
+
+

设置含转义字符的参数值

+

以命令行方式设置参数时,若参数值中含有属于 omegaconf escaping characters 的转义字符(\\, [, ], {, }, (, ), :, =, \),则推荐使用 \' 将参数值包围起来,保证内部的字符不被转义,否则可能在 hydra 解析参数时引起报错,或以不正确的方式运行程序。假设我们在运行时需要指定 PATH/workspace/lr=0.1,s=[3]/best_model.pdparams,该路径含有转义字符 [, ]=,因此则可以按照如下方式撰写参数。

+
# 正确的参数指定方式如下
+python example.py PATH=\'/workspace/lr=0.1,s=[3]/best_model.pdparams\'
+
+# 错误的参数指定方式如下
+# python example.py PATH=/workspace/lr=0.1,s=[3]/best_model.pdparams
+# python example.py PATH='/workspace/lr=0.1,s=[3]/best_model.pdparams'
+# python example.py PATH="/workspace/lr=0.1,s=[3]/best_model.pdparams"
+
+
+

1.1.3 自动化运行实验

+

1.1.2 命令行方式配置参数 所述,可以通过在程序执行命令的末尾加上合适的参数来控制多组实验的运行配置,接下来以自动化执行四组实验为例,介绍如何利用 hydra 的 multirun 功能,实现该目的。

+

假设这四组实验围绕随机种子 seed 和训练轮数 epochs 进行配置,组合如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
实验编号seedepochs
14210
24220
3102410
4102420
+

执行如下命令即可按顺序自动运行这 4 组实验。

+
$ python bracket.py -m seed=42,1024 TRAIN.epochs=10,20
[HYDRA] Launching 4 jobs locally
+[HYDRA]        #0 : seed=42 TRAIN.epochs=10
+...
+[HYDRA]        #1 : seed=42 TRAIN.epochs=20
+...
+[HYDRA]        #2 : seed=1024 TRAIN.epochs=10
+...
+[HYDRA]        #3 : seed=1024 TRAIN.epochs=20
+...
+
+

多组实验各自的参数文件、日志文件则保存在以不同参数组合为名称的子文件夹中,如下所示。

+
$ tree PaddleScience/examples/bracket/outputs_bracket/
PaddleScience/examples/bracket/outputs_bracket/
+└── 2023-10-14 # (1)
+    └── 04-01-52 # (2)
+        ├── TRAIN.epochs=10,20,seed=42,1024 # multirun 总配置保存目录
+           └── multirun.yaml # multirun 配置文件 (3)
+        ├── TRAIN.epochs=10,seed=1024 # 实验编号3的保存目录
+           ├── checkpoints
+              ├── latest.pdeqn
+              ├── latest.pdopt
+              ├── latest.pdparams
+              └── latest.pdstates
+           ├── train.log
+           └── visual
+               └── epoch_0
+                   └── result_u_v_w_sigmas.vtu
+        ├── TRAIN.epochs=10,seed=42 # 实验编号1的保存目录
+           ├── checkpoints
+              ├── latest.pdeqn
+              ├── latest.pdopt
+              ├── latest.pdparams
+              └── latest.pdstates
+           ├── train.log
+           └── visual
+               └── epoch_0
+                   └── result_u_v_w_sigmas.vtu
+        ├── TRAIN.epochs=20,seed=1024 # 实验编号4的保存目录
+           ├── checkpoints
+              ├── latest.pdeqn
+              ├── latest.pdopt
+              ├── latest.pdparams
+              └── latest.pdstates
+           ├── train.log
+           └── visual
+               └── epoch_0
+                   └── result_u_v_w_sigmas.vtu
+        └── TRAIN.epochs=20,seed=42 # 实验编号2的保存目录
+            ├── checkpoints
+               ├── latest.pdeqn
+               ├── latest.pdopt
+               ├── latest.pdparams
+               └── latest.pdstates
+            ├── train.log
+            └── visual
+                └── epoch_0
+                    └── result_u_v_w_sigmas.vtu
+
+
    +
  1. 该文件夹是程序运行时根据日期自动创建得到,此处表示2023年10月14日
  2. +
  3. 该文件夹是程序运行时根据运行时刻(世界标准时间,UTC)自动创建得到,此处表示04点01分52秒
  4. +
  5. 该文件夹是 multirun 模式下额外产生一个总配置目录,主要用于保存 multirun.yaml,其内的 hydra.overrides.task 字段记录了用于组合出不同运行参数的原始配置。
  6. +
+

考虑到用户的阅读和学习成本,本章节只介绍了常用的实验方法,更多进阶用法请参考 hydra官方教程

+

1.2 模型导出

+

1.2.1 Paddle 推理模型导出

+
+

Warning

+

少数案例尚未支持导出功能,因此对应文档中未给出导出命令。

+
+

在训练完毕后,我们通常需要将模型导出为 *.pdmodel, *.pdiparams, *.pdiparams.info 三个文件,以便后续推理部署使用。以 Aneurysm 案例为例,导出模型的通用命令如下。

+
python aneurysm.py mode=export \
+    INFER.pretrained_model_path="https://paddle-org.bj.bcebos.com/paddlescience/models/aneurysm/aneurysm_pretrained.pdparams"
+
+
+

Tip

+

由于支持模型导出的案例的 YAML 文件已经将 INFER.pretrained_model_path 的默认值设置为官方提供的预训练模型地址,因此导出官方提供的预训练模型时可以在命令行中省略 INFER.pretrained_model_path=... 参数。

+
+

根据终端输出信息,导出的模型会被保存在执行导出命令所在目录的相对路径:./inference/ 文件夹下,如下所示。

+
...
+ppsci MESSAGE: Inference model has been exported to: ./inference/aneurysm, including *.pdmodel, *.pdiparams and *.pdiparams.info files.
+
+
./inference/
+├── aneurysm.pdiparams
+├── aneurysm.pdiparams.info
+└── aneurysm.pdmodel
+
+

1.2.2 ONNX 推理模型导出

+

在导出 ONNX 推理模型前,需要完成 1.2.1 Paddle 推理模型导出 的步骤,得到inference/aneurysm.pdiparamsinference/aneurysm.pdmodel

+

然后安装 paddle2onnx。

+
pip install paddle2onnx
+
+

接下来仍然以 aneurysm 案例为例,介绍命令行直接导出和 PaddleScience 导出两种方式。

+
+
+
+
paddle2onnx \
+    --model_dir=./inference/ \
+    --model_filename=aneurysm.pdmodel \
+    --params_filename=aneurysm.pdiparams \
+    --save_file=./inference/aneurysm.onnx \
+    --opset_version=13 \
+    --enable_onnx_checker=True
+
+

若导出成功,输出信息如下所示

+
[Paddle2ONNX] Start to parse PaddlePaddle model...
+[Paddle2ONNX] Model file path: ./inference/aneurysm.pdmodel
+[Paddle2ONNX] Paramters file path: ./inference/aneurysm.pdiparams
+[Paddle2ONNX] Start to parsing Paddle model...
+[Paddle2ONNX] Use opset_version = 13 for ONNX export.
+[Paddle2ONNX] PaddlePaddle model is exported as ONNX format now.
+2024-03-02 05:45:12 [INFO]      ===============Make PaddlePaddle Better!================
+2024-03-02 05:45:12 [INFO]      A little survey: https://iwenjuan.baidu.com/?code=r8hu2s
+
+
+
+

在 aneurysm.py 中的export函数中,将with_onnx参数改为True

+
def export(cfg: DictConfig):
+    # set model
+    model = ppsci.arch.MLP(**cfg.MODEL)
+
+    # initialize solver
+    solver = ppsci.solver.Solver(
+        model,
+        pretrained_model_path=cfg.INFER.pretrained_model_path,
+    )
+    # export model
+    from paddle.static import InputSpec
+
+    input_spec = [
+        {key: InputSpec([None, 1], "float32", name=key) for key in model.input_keys},
+    ]
+    solver.export(input_spec, cfg.INFER.export_path, with_onnx=True)
+
+

然后执行模型导出命令。

+
python aneurysm.py mode=export
+
+

若导出成功,输出信息如下所示。

+
...
+[Paddle2ONNX] Start to parse PaddlePaddle model...
+[Paddle2ONNX] Model file path: ./inference/aneurysm.pdmodel
+[Paddle2ONNX] Paramters file path: ./inference/aneurysm.pdiparams
+[Paddle2ONNX] Start to parsing Paddle model...
+[Paddle2ONNX] Use opset_version = 13 for ONNX export.
+[Paddle2ONNX] PaddlePaddle model is exported as ONNX format now.
+ppsci MESSAGE: ONNX model has been exported to: ./inference/aneurysm.onnx
+
+
+
+
+

1.3 模型推理预测

+

1.3.1 动态图推理

+

若需使用训练完毕保存或下载得到的模型文件 *.pdparams 直接进行推理(预测),可以参考以下代码示例。

+
    +
  1. +

    加载 *.pdparams 文件内的参数到模型中

    +
    import ppsci
    +import numpy as np
    +
    +# 实例化一个输入为 (x,y,z) 三个维度上的坐标,输出为 (u,v,w) 三个维度上的速度的 model
    +model = ppsci.arch.MLP(("x", "y", "z"), ("u", "v", "w"), 5, 64, "tanh")
    +
    +# 用该模型及其对应的预训练模型路径(或下载地址 url)两个参数初始化 solver
    +solver = ppsci.solver.Solver(
    +    model=model,
    +    pretrained_model_path="/path/to/pretrained.pdparams",
    +)
    +# 在 Solver(...) 中会自动从给定的 pretrained_model_path 加载(下载)参数并赋值给 model 的对应参数
    +
    +
  2. +
  3. +

    准备好用于预测的输入数据,并以字典 dict 的方式传递给 solver.predict

    +
    N = 100 # 假设要预测100个样本的结果
    +x = np.random.randn(N, 1) # 输入数据x
    +y = np.random.randn(N, 1) # 输入数据y
    +z = np.random.randn(N, 1) # 输入数据z
    +
    +input_dict = {
    +    "x": x,
    +    "y": y,
    +    "z": z,
    +}
    +
    +output_dict = solver.predict(
    +    input_dict,
    +    batch_size=32, # 推理时的 batch_size
    +    return_numpy=True, # 返回结果是否转换为 numpy
    +)
    +
    +# output_dict 预测结果同样以字典的形式保存在 output_dict 中,其具体内容如下
    +for k, v in output_dict.items():
    +    print(f"{k} {v.shape}")
    +# "u": (100, 1)
    +# "v": (100, 1)
    +# "w": (100, 1)
    +
    +
  4. +
+

1.3.2 Inference 推理(python)

+
+

Paddle Inference 是飞桨的原生推理库,相比 1.3.1 动态图推理 具有更快的推理速度,适合不同平台不同应用场景的快速部署,详细信息可参考: Paddle Inference 文档

+
+
+

Warning

+

少数案例尚未支持导出、推理功能,因此对应文档中未给出导出、推理命令。

+
+

首先需参考 1.2 模型导出 章节,从 *.pdparams 文件导出 *.pdmodel, *.pdiparams 两个文件。

+

Aneurysm 案例为例,假设导出后的模型文件以 ./inference/aneurysm.* 的形式保存,则推理代码示例如下。

+
# linux
+wget -nc https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar
+# windows
+# curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/aneurysm/aneurysm_dataset.tar -o aneurysm_dataset.tar
+# unzip it
+tar -xvf aneurysm_dataset.tar
+python aneurysm.py mode=infer
+
+

输出信息如下:

+
...
+...
+ppsci INFO: Predicting batch 2880/2894
+ppsci INFO: Predicting batch 2894/2894
+ppsci MESSAGE: Visualization result is saved to: ./aneurysm_pred.vtu
+
+

1.3.3 使用不同的推理配置

+

PaddleScience 提供了多种推理配置组合,可通过命令行进行组合,目前支持的推理配置如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NativeONNXTensorRTMKLDNN
CPU/
GPU/
XPUTODO///
+

接下来以 aneurysm 案例和 Linux x86_64 + TensorRT 8.6 GA + CUDA 11.6 软硬件环境为例,介绍如何使用不同的推理配置。

+
+
+
+

Paddle 提供了原生推理功能,支持 CPU 和 GPU。

+

运行以下命令进行推理:

+
# CPU
+python aneurysm.py mode=infer \
+    INFER.device=cpu \
+    INFER.engine=native
+
+# GPU
+python aneurysm.py mode=infer \
+    INFER.device=gpu \
+    INFER.engine=native
+
+
+
+

TensorRT 是英伟达推出的高性能推理引擎,适用于 GPU 推理加速,PaddleScience 支持了 TensorRT 推理功能。

+
    +
  1. +

    根据你的软硬件环境,下载并解压对应的 TensorRT 推理库压缩包(.tar 文件):https://developer.nvidia.com/tensorrt#。 +推荐使用 TensorRT 8.x、7.x 等较新的版本

    +
  2. +
  3. +

    在解压完毕的文件中,找到 libnvinfer.so 文件所在的目录,将其加入到 LD_LIBRARY_PATH 环境变量中。

    +
    TRT_PATH=/PATH/TO/TensorRT-8.6.1.6
    +find $TRT_PATH -name libnvinfer.so
    +
    +# /PATH/TO/TensorRT-8.6.1.6/targets/x86_64-linux-gnu/lib/libnvinfer.so   <---- use this path
    +export LD_LIBRARY_PATH=/PATH/TO/TensorRT-8.6.1.6/targets/x86_64-linux-gnu/lib/:$LD_LIBRARY_PATH
    +
    +
  4. +
  5. +

    运行 aneurysm.py 的推理功能,同时指定推理引擎为 TensorRT。

    +
    # 运行前需设置指定GPU,否则可能无法启动 TensorRT
    +export CUDA_VISIBLE_DEVICES=0
    +
    +python aneurysm.py mode=infer \
    +    INFER.device=gpu \
    +    INFER.engine=tensorrt \
    +    INFER.min_subgraph_size=5
    +
    +
  6. +
+
+
+

ONNX 是微软开源的深度学习推理框架,PaddleScience 支持了 ONNX 推理功能。

+

首先按照 1.2.2 ONNX 推理模型导出 章节将 *.pdmodel*.pdiparams 转换为 *.onnx 文件, +然后根据硬件环境,安装 CPU 或 GPU 版的 onnxruntime:

+
pip install onnxruntime  # CPU
+pip install onnxruntime-gpu  # GPU
+
+

最后运行以下命令进行推理:

+
# CPU
+python aneurysm.py mode=infer \
+    INFER.device=cpu \
+    INFER.engine=onnx
+
+# GPU
+python aneurysm.py mode=infer \
+    INFER.device=gpu \
+    INFER.engine=onnx
+
+
+
+

MKLDNN 是英伟达推出的高性能推理引擎,适用于 CPU 推理加速,PaddleScience 支持了 MKLDNN 推理功能。

+

运行以下命令进行推理:

+
python aneurysm.py mode=infer \
+    INFER.device=cpu \
+    INFER.engine=mkldnn
+
+
+
+
+
+

完整推理配置参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数默认值说明
INFER.devicecpu推理设备,目前支持 cpugpu
INFER.enginenative推理引擎,目前支持 native, tensorrt, onnxmkldnn
INFER.precisionfp32推理精度,目前支持 fp32, fp16
INFER.ir_optimTrue是否启用 IR 优化
INFER.min_subgraph_size30TensorRT 中最小子图 size,当子图的 size 大于该值时,才会尝试对该子图使用 TensorRT 计算
INFER.gpu_mem2000初始显存大小
INFER.gpu_id0GPU 逻辑设备号
INFER.max_batch_size1024推理时的最大 batch_size
INFER.num_cpu_threads10MKLDNN 和 ONNX 在 CPU 推理时的线程数
INFER.batch_size256推理时的 batch_size
+
+

1.4 断点继续训练

+

在模型的日常训练中,可能存在机器故障或者用户手动操作而中断训练的情况,针对这种情况 PaddleScience 提供了断点继续训练的功能,即在训练时默认会保存最近一个训练完毕的 epoch 对应的各种参数到以下 5 个文件中:

+
    +
  1. latest.pdparams,该文件保存了神经网络模型的所有权重参数。
  2. +
  3. latest.pdopt,该文件保存了优化器(如 Adam 等一些带有动量记录功能的优化器)的所有参数。
  4. +
  5. latest.pdeqn,该文件保存了所有方程的参数,在一些逆问题中如果方程本身含有待估计(可学习)的参数,那么该文件就会保存这些参数。
  6. +
  7. latest.pdstates,该文件保存了 latest 对应 epoch 的所有评估指标以及 epoch 数。
  8. +
  9. latest.pdscaler(可选),在开启自动混合精度(AMP)功能时,该文件保存了 GradScaler 梯度缩放器内部的参数。
  10. +
+
+
+
+

若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 TRAIN.checkpoint_pathlatest.* 的所在路径(建议用\'包裹),再执行训练命令即可,免去修改案例代码。

+
python example.py TRAIN.checkpoint_path=\'/path/to/latest\'
+
+
+
+

只需要在 Solver 时指定 checkpoint_path 参数为 latest.* 的所在路径,即可自动载入上述的几个文件,并从 latest 中记录的 epoch 开始继续训练。

+
import ppsci
+
+...
+
+solver = ppsci.solver.Solver(
+    ...,
+    checkpoint_path="/path/to/latest"
+)
+
+
+
+
+
+

路径填写注意事项

+

此处只需将路径填写到 "latest" 为止即可,不需要加上其后缀,程序会根据 "/path/to/latest",自动补充不同文件对应的后缀名来加载 latest.pdparamslatest.pdopt 等文件。

+
+

1.5 迁移学习

+

迁移学习是一种广泛使用、低成本提高模型精度的训练方式。在 PaddleScience 中,可以通过在 model 实例化完毕之后,手动为其载入预训练模型权重后开始微调训练;也可以调用 Solver.finetune 接口并指定 pretrained_model_path 参数,自动载入预训练模型权重并开始微调训练。

+
+
+
+

若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 TRAIN.pretrained_model_path 为预训练权重的所在路径(建议用\'包裹),再执行训练命令即可,免去修改案例代码。

+
python example.py TRAIN.pretrained_model_path=\'/path/to/pretrain\'
+
+
+
+
import ppsci
+from ppsci.utils import save_load
+
+...
+...
+
+model = ...
+save_load.load_pretrain(model, "/path/to/pretrain")
+solver = ppsci.solver.Solver(
+    ...,
+)
+solver.train()
+
+
+
+
import ppsci
+
+
+...
+...
+
+model = ...
+solver = ppsci.solver.Solver(
+    ...,
+)
+solver.finetune(pretrained_model_path="/path/to/pretrain")
+
+
+
+
+
+

迁移学习建议

+

在迁移学习时,相对于完全随机初始化的参数而言,载入的预训练模型权重参数是一个较好的初始化状态,因此不需要使用太大的学习率,而可以将学习率适当调小 2~10 倍以获得更稳定的训练过程和更好的精度。

+
+

1.6 模型评估

+

当模型训练完毕之后,如果想手动对某一个模型权重文件,评估其在数据集上的精度,则可以选择下面的几种方式之一进行评估。

+
+
+
+

若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 EVAL.pretrained_model_path 为待评估模型权重的所在路径(建议用\'包裹),并指定模式为eval后,执行评估命令即可,免去修改案例代码。

+
python example.py mode=eval EVAL.pretrained_model_path=\'/path/to/pretrain\'
+
+
+
+

Solver 实例化时指定参数 pretrained_model_path 为该权重文件的路径,然后调用 Solver.eval() 即可。

+
import ppsci
+import ppsci.utils
+
+...
+...
+
+solver = ppsci.solver.Solver(
+    ...,
+    ...,
+    pretrained_model_path="/path/to/model"
+)
+solver.eval()
+
+
+
+
+

1.7 实验过程可视化

+
+
+
+

TensorBoardX 是基于 TensorBoard 编写可视化分析工具,以丰富的图表呈现训练参数变化趋势、数据样本、模型结构、PR曲线、ROC曲线、高维数据分布等。帮助用户清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型调优。

+

PaddleScience 支持使用 TensorBoardX 记录训练过程中的基础实验数据,包括 train/eval loss,eval metric,learning rate 等基本信息,可按如下步骤使用该功能。

+
    +
  1. +

    安装 Tensorboard 和 TensorBoardX

    +
    pip install tensorboard tensorboardX
    +
    +
  2. +
  3. +

    在案例中启用 tensorboardX

    +
    +
    +
    +

    若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 use_tbd 再执行训练命令即可,免去修改案例代码。

    +
    python example.py use_tbd=True
    +
    +
    +
    +
    solver = ppsci.solver.Solver(
    +    ...,
    +    use_tbd=True,
    +)
    +
    +
    +
    +
    +
  4. +
  5. +

    可视化记录数据

    +

    根据上述步骤,在训练时 TensorBoardX 会自动记录数据并保存到 ${solver.output_dir}/tensorboard 目录下,具体所在路径在实例化 Solver 时,会自动打印在终端中,如下所示。

    +
    ppsci MESSAGE: TensorboardX tool is enabled for logging, you can view it by running:
    +tensorboard --logdir outputs_VIV/2024-01-01/08-00-00/tensorboard
    +
    +
    +

    Tip

    +

    也可以输入 tensorboard --logdir ./outputs_VIV,一次性在网页上展示 outputs_VIV 目录下所有训练记录,便于对比。

    +
    +

    在终端里输入上述可视化命令,并用浏览器进入 TensorBoardX 给出的可视化地址,即可在浏览器内查看记录的数据,如下图所示。

    +

    tensorboardx_preview

    +
  6. +
+
+
+

VisualDL 是飞桨推出的可视化分析工具,以丰富的图表呈现训练参数变化趋势、数据样本、模型结构、PR曲线、ROC曲线、高维数据分布等。帮助用户清晰直观地理解深度学习模型训练过程及模型结构,进而实现高效的模型调优。

+

PaddleScience 支持使用 VisualDL 记录训练过程中的基础实验数据,包括 train/eval loss,eval metric,learning rate 等基本信息,可按如下步骤使用该功能。

+
    +
  1. +

    安装 VisualDL

    +
    pip install -U visualdl
    +
    +
  2. +
  3. +

    在案例中启用 visualDL

    +
    +
    +
    +

    若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 use_vdl 再执行训练命令即可,免去修改案例代码。

    +
    python example.py use_vdl=True
    +
    +
    +
    +
    solver = ppsci.solver.Solver(
    +    ...,
    +    use_vdl=True,
    +)
    +
    +
    +
    +
    +
  4. +
  5. +

    可视化记录数据

    +

    根据上述步骤,在训练时 VisualDL 会自动记录数据并保存到 ${solver.output_dir}/vdl 目录下,具体所在路径在实例化 Solver 时,会自动打印在终端中,如下所示。

    +
    Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.8, Runtime API Version: 11.6
    +device: 0, cuDNN Version: 8.4.
    +ppsci INFO: VisualDL tool enabled for logging, you can view it by running:
    +visualdl --logdir outputs_darcy2d/2023-10-08/10-00-00/TRAIN.epochs=400/vdl --port 8080
    +
    +

    在终端里输入上述可视化命令,并用浏览器进入 VisualDL 给出的可视化地址,即可在浏览器内查看记录的数据,如下图所示。

    +

    visualdl_record

    +
  6. +
+
+
+

WandB 是一个第三方实验记录工具,能在记录实验数据的同时将数据上传到用户的私人账户上,防止实验记录丢失。

+

PaddleScience 支持使用 WandB 记录基本的实验数据,包括 train/eval loss,eval metric,learning rate 等基本信息,可按如下步骤使用该功能

+
    +
  1. +

    安装 wandb

    +
    pip install wandb
    +
    +
  2. +
  3. +

    注册 wandb 并在终端登录

    +
    # 登录 wandb 获取 API key
    +wandb login
    +# 根据 login 提示,输入 API key 并回车确认
    +
    +
  4. +
  5. +

    在案例中启用 wandb

    +
    +
    +
    +

    若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 use_wandb 再执行训练命令即可,免去修改案例代码。

    +
    python example.py use_wandb=True
    +
    +
    +
    +
    solver = ppsci.solver.Solver(
    +    ...,
    +    use_wandb=True,
    +    wandb_config={
    +        "project": "PaddleScience",
    +        "name": "Laplace2D",
    +        "dir": OUTPUT_DIR,
    +    },
    +    ...
    +)
    +solver.train()
    +
    +
    +
    +
    +

    如上述代码所示,指定 use_wandb=True,并且设置 wandb_config 配置字典中的 projectnamedir 三个字段,然后启动训练即可。训练过程会实时上传记录数据至 wandb 服务器,训练结束后可以进入终端打印的预览地址在网页端查看完整训练记录曲线。

    +
    +

    注意

    +

    由于每次调用 wandb.log 会使得其自带的计数器 Step 自增 1,因此在 wandb 的网站上查看训练记录时,需要手动更改 x 轴的单位为 step(全小写),如下所示。

    +

    否则默认单位为 wandb 自带的 Step (S大写) 字段,会导致显示步数比实际步数多几倍。 +wandb_step settings

    +
    +
  6. +
+
+
+
+

2. 进阶功能

+

2.1 贝叶斯超参搜索

+

hydra 的自动化实验功能可以与 optuna 超参数调优工具一起使用。在 yaml 文件中设置好需要调整的参数和最大实验次数后,可以调用 Tree-structured Parzen Estimator(TPE) 算法进行自动化调参工具,效率高于网格调参法。

+

下面以 viv 案例为例,介绍如何在 PaddleScience 中使用该方法。

+
    +
  1. +

    安装 1.1.0 以上版本的 hydra-core 以及 hydra-optuna 插件

    +
    pip install 'hydra-core>=1.1.0' hydra-optuna-sweeper
    +
    +
  2. +
  3. +

    修改 viv.yaml 文件,在 defaults:hydra: 字段下分别添加如下配置(高亮部分所示)

    +
    viv.yaml
    defaults:
    +  - ppsci_default
    +  - TRAIN: train_default
    +  - TRAIN/ema: ema_default
    +  - TRAIN/swa: swa_default
    +  - EVAL: eval_default
    +  - INFER: infer_default
    +  - override hydra/sweeper: optuna # (1)
    +  - _self_
    +
    +hydra:
    +  run:
    +    # dynamic output directory according to running time and override name
    +    dir: outputs_VIV/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname}
    +  job:
    +    name: ${mode} # name of logfile
    +    chdir: false # keep current working directory unchanged
    +  callbacks:
    +    init_callback:
    +      _target_: ppsci.utils.callbacks.InitCallback
    +  sweep:
    +    # output directory for multirun
    +    dir: ${hydra.run.dir}
    +    subdir: ./
    +
    +  sweeper: # (2)
    +    direction: minimize # (3)
    +    study_name: viv_optuna # (4)
    +    n_trials: 20 # (5)
    +    n_jobs: 1 # (6)
    +    params: # (7)
    +      MODEL.num_layers: choice(2, 3, 4, 5, 6, 7) # (8)
    +      TRAIN.lr_scheduler.learning_rate: interval(0.0001, 0.005) # (9)
    +
    +
      +
    1. 指定了使用 optuna 进行超参数优化。
    2. +
    3. sweeper::这一行指定了 Hydra 使用的 sweeper 插件,用于进行参数扫描。在这个例子中,它将使用 Optuna 进行超参数优化。
    4. +
    5. direction: minimize:这指定了优化的目标方向。minimize 表示我们希望最小化目标函数(例如模型的验证损失)。如果我们希望最大化某个指标(例如准确率),则可以设置为 maximize。
    6. +
    7. study_name: viv_optuna:这设置了 Optuna 研究(Study)的名称。这个名称用于标识和引用特定的研究,有助于在以后的分析或继续优化时跟踪结果。
    8. +
    9. n_trials: 20:这指定了要运行的总试验次数。在这个例子中,Optuna 将执行 20 次独立的试验来寻找最佳的超参数组合。
    10. +
    11. n_jobs: 1:这设置了可以并行运行的试验数量。值为 1 意味着试验将依次执行,而不是并行。如果你的系统有多个 CPU 核心,并且希望并行化以加速搜索过程,可以将这个值设置为更高的数字或 -1(表示使用所有可用的 CPU 核心)。
    12. +
    13. params:: 这一节定义了要优化的超参数以及它们的搜索空间。
    14. +
    15. MODEL.num_layers: choice(2, 3, 4, 5, 6, 7):这指定了模型层数的可选值。choice 函数表示 Optuna 在 2, 3, 4, 5, 6, 和 7 中随机选择一个值。
    16. +
    17. TRAIN.lr_scheduler.learning_rate: interval(0.0001, 0.005):这指定了学习率的搜索范围。interval 表示学习率的值将在 0.0001 和 0.005 之间均匀分布地选择。
    18. +
    +

    如上所示,在 hydra.sweeper 节点下添加了 optuna 插件的配置,并在 params 节点下指定了要调优的参数及其范围: +1. 模型层数 MODEL.num_layers,在 [2, 3, 4, 5, 6, 7] 共 6 种层数间进行调优。 +2. 学习率 TRAIN.lr_scheduler.learning_rate,在 0.0001 ~ 0.005 之间进行调优。

    +
    +

    注意

    +
      +
    1. 调优的参数需要与 yaml 文件中配置的参数名一致,如 MODEL.num_layersTRAIN.lr_scheduler.learning_rate
    2. +
    3. 调优的参数范围根据不同语义进行指定,比如模型层数必须为整数,可以使用 choice(...) 设置有限范围,而学习率一般为浮点数,可以使用 interval(...) 设置其上下界。
    4. +
    +
    +
  4. +
  5. +

    修改 viv.py,使得被 @hydra.main 装饰的 main 函数返回实验指标结果(高亮部分所示)

    +
    viv.py
    def train(cfg: DictConfig):
    +    ...
    +    # initialize solver
    +    solver = ppsci.solver.Solver(
    +        model,
    +        equation=equation,
    +        validator=validator,
    +        visualizer=visualizer,
    +        cfg=cfg,
    +    )
    +
    +    # evaluate
    +    l2_err_eval, _ = solver.eval()
    +    return l2_err_eval
    +
    +...
    +
    +@hydra.main(version_base=None, config_path="./conf", config_name="viv.yaml")
    +def main(cfg: DictConfig):
    +    if cfg.mode == "train":
    +        return train(cfg)
    +    elif cfg.mode == "eval":
    +        evaluate(cfg)
    +
    +
  6. +
  7. +

    运行以下命令,开始自动化调优

    +
    python viv.py --multirun
    +
    +
  8. +
+

在 20 次调优实验运行完毕后,在模型保存目录下,会生成 optimization_results.yaml 文件,其中包含了最佳的调优结果,如下所示:

+
name: optuna
+best_params:
+  MODEL.num_layers: 7
+  TRAIN.lr_scheduler.learning_rate: 0.003982453338298202
+best_value: 0.02460772916674614
+
+

更多详细信息以及多目标自动调优方法,可参考:Optuna Sweeper pluginOptuna

+

2.2 分布式训练

+

2.2.1 数据并行

+

接下来以 examples/pipe/poiseuille_flow.py 为例,介绍如何正确使用 PaddleScience 的数据并行功能。分布式训练细节可以参考:Paddle-使用指南-分布式训练-快速开始-数据并行

+
    +
  1. +

    在 constraint 实例化完毕后,将 ITERS_PER_EPOCH 重新赋值为经过自动多卡数据切分后的 dataloader 的长度(一般情况下其长度等于单卡 dataloader 的长度除以卡数,向上取整),如代码中高亮行所示。

    +
    examples/pipe/poiseuille_flow.py
    ITERS_PER_EPOCH = int((N_x * N_y * N_p) / BATCH_SIZE)
    +
    +pde_constraint = ppsci.constraint.InteriorConstraint(
    +    equation["NavierStokes"].equations,
    +    {"continuity": 0, "momentum_x": 0, "momentum_y": 0},
    +    geom=interior_geom,
    +    dataloader_cfg={
    +        "dataset": "NamedArrayDataset",
    +        "num_workers": 1,
    +        "batch_size": BATCH_SIZE,
    +        "iters_per_epoch": ITERS_PER_EPOCH,
    +        "sampler": {
    +            "name": "BatchSampler",
    +        },
    +    },
    +    loss=ppsci.loss.MSELoss("mean"),
    +    evenly=True,
    +    name="EQ",
    +)
    +ITERS_PER_EPOCH = len(pde_constraint.data_loader) # re-assign to ITERS_PER_EPOCH
    +
    +# wrap constraints together
    +constraint = {pde_constraint.name: pde_constraint}
    +
    +EPOCHS = 3000 if not args.epochs else args.epochs
    +
    +
  2. +
  3. +

    使用分布式训练命令启动训练,以 4 卡数据并行训练为例

    +
    # 指定 0,1,2,3 张卡启动分布式数据并行训练
    +export CUDA_VISIBLE_DEVICES=0,1,2,3
    +python -m paddle.distributed.launch --gpus="0,1,2,3" poiseuille_flow.py
    +
    +
  4. +
+ + +

2.3 自动混合精度训练

+

接下来介绍如何正确使用 PaddleScience 的自动混合精度功能。自动混合精度的原理可以参考:Paddle-使用指南-性能调优-自动混合精度训练(AMP)

+

若想在训练中启用自动混合精度,则可以选择下面的几种方式之一。O1 为自动混合精度,O2 为更激进的纯 fp16 训练模式,一般推荐使用 O1

+
+
+
+

若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 use_ampamp_level 再执行训练/评估命令即可,免去修改案例代码。

+
python example.py use_amp=True amp_level=O1
+
+
+
+

实例化 Solver 时加上 2 个参数: use_amp=True, amp_level="O1"(或amp_level="O2")。如代码中高亮行所示,通过指定 use_amp=True,开启自动混合精度功能,接着再设置 amp_level="O1",指定混合精度所用的模式。

+
# initialize solver
+solver = ppsci.solver.Solver(
+    ...,
+    ...,
+    use_amp=True,
+    amp_level="O1", # or amp_level="O2"
+)
+
+
+
+
+

2.4 梯度累加

+

接下来介绍如何正确使用 PaddleScience 的梯度累加功能。梯度累加的原理可以参考:Paddle-使用指南-性能调优-自动混合精度训练(AMP)-动态图下使用梯度累加

+

实例化 Solver 时指定 update_freq 参数为大于 1 的正整数即可。如代码中高亮行所示,update_freq 可以设置为 2 或者更大的整数,推荐使用 2、4、8,此时对于训练任务来说,全局 batch size 等价于 update_freq * batch size。梯度累加方法在大多数场景中能够让间接地扩大每个 batch 内的样本数量,从而让每个 batch 分布更接近真实数据分布,提升训练任务的性能。

+
+
+
+

若在案例代码中,为 Solver 构建时传递了 cfg 参数,则可以通过命令行指定 TRAIN.update_freq 再执行训练命令即可,免去修改案例代码。

+
python example.py TRAIN.update_freq=2
+
+
+
+
# initialize solver
+solver = ppsci.solver.Solver(
+    ...,
+    ...,
+    update_freq=2, # or 4, 8
+)
+
+
+
+
+

2.5 多任务学习

+

在机理驱动、数理融合场景中,往往会同时优化多个损失项,如控制方程残差损失、(初)边值条件损失等。在训练过程中这些损失项对参数的梯度方向可能会互相冲突,阻碍训练精度收敛,而这正是多任务学习方法能解决的问题。因此 PaddleScience 在多任务学习模块中引入了几种常见的算法,其主要通过对不同任务的权重或产生的梯度进行调整,从而缓解该问题,最终提升模型收敛精度。下面以 Relobralo 算法进行举例,使用方式如下:

+
    +
  1. +

    实例化一个多任务学习方法的对象

    +
    from ppsci.loss import mtl
    +model = ...
    +num_losses = 2 # number of losses to be optimized
    +loss_aggregator = mtl.Relobralo(num_losses)
    +
    +
  2. +
  3. +

    将该对象作为 Solver 的实例化参数之一传入

    +
    solver = ppsci.solver.Solver(
    +    ...,
    +    ...,
    +    loss_aggregator=loss_aggregator,
    +)
    +
    +
  4. +
  5. +

    启动训练,训练过程中 loss_aggregator 会自动对获取到的多个损失项应用对应的多任务学习方法进行优化

    +
    solver.train()
    +
    +
    +

    影响说明

    +

    个别多任务学习方法(如weight based method)可能会改变训练过程中损失函数的计算方式,但仅限于影响训练过程,模型评估过程的损失计算方式保持不变。

    +
    +
  6. +
+

3. 使用 Nsight 进行性能分析

+

Nsight是NVIDIA面向开发者提供的开发工具套件,能提供深入的跟踪、调试、评测和分析,以优化跨 NVIDIA GPU和CPU的复杂计算应用程序。详细文档可参考:Nsight Systems Document

+

PaddleScience 初步支持使用 Nsight 进行性能分析,以 linux 开发环境 + laplace2d 案例为例,按照如下步骤使用 nsight 工具生成性能分析报告并查看分析结果。

+
    +
  1. +

    安装 nsight-system

    +

    开发机上下载 linux nsight-system 软件:nsight-systems/2023.4.1,并将 nsight 添加到环境变量 PATH 中:

    +

    执行:PATH=/path/to/nsight-systems/2023.4.1/bin:$PATH,同时在 windows 机器上安装相同版本的 nsight-system 软件。

    +
  2. +
  3. +

    用 nsys 命令运行程序,生成性能分析文件

    +
    NVTX=1 nsys profile -t cuda,nvtx --stats=true -o laplace2d python laplace2d.py
    +
    +
  4. +
  5. +

    查看分析结果

    +

    程序结束后,在终端内会打印出性能分析数据(如下所示),同时在上述 -o 参数指定的相对文件路径生成 laplace2d.nsys-replaplace2d.sqlite 两个文件。

    +

    在 windows 上使用 NVIDIA Nsight Systems 软件打开 laplace2d.nsys-rep,即可在图形化的界面上查看性能分析数据。

    +
    ...
    +...
    +Only run 25 steps when 'NVTX' is set in environment for nsight analysis. Exit now ......
    +
    +Generating '/tmp/nsys-report-18e4.qdstrm'
    +[1/7] [========================100%] laplace2d.nsys-rep
    +[2/7] [========================100%] laplace2d.sqlite
    +[3/7] Executing 'nvtx_sum' stats report
    +
    +Time (%)  Total Time (ns)  Instances    Avg (ns)       Med (ns)      Min (ns)     Max (ns)     StdDev (ns)    Style                  Range
    +--------  ---------------  ---------  -------------  -------------  -----------  -----------  -------------  -------  ------------------------------------
    +    15.1      794,212,341         25   31,768,493.6    5,446,410.0    5,328,471  661,841,104  131,265,333.9  PushPop  Loss computation
    +    14.5      766,452,142         25   30,658,085.7    4,369,873.0    4,281,927  659,795,434  131,070,475.4  PushPop  Constraint EQ
    +    13.0      687,324,359      1,300      528,711.0       32,567.5       21,218  641,625,892   17,794,532.4  PushPop  matmul dygraph
    +    12.9      678,475,194          1  678,475,194.0  678,475,194.0  678,475,194  678,475,194            0.0  PushPop  Training iteration 1
    +    12.8      673,614,062      1,300      518,164.7       19,802.5       14,499  641,525,121   17,792,027.2  PushPop  matmul compute
    +    3.9      203,945,648         25    8,157,825.9    8,029,819.0    7,797,185    9,119,496      359,173.3  PushPop  Loss backward
    +    ...
    +    ...
    +
    +
  6. +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file