From 8fbe932146d6414fdbf69377b8b5a6eb16407bd6 Mon Sep 17 00:00:00 2001 From: abdullah-cognite <100700755+abdullah-cognite@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:12:30 +0200 Subject: [PATCH 1/2] remove LICENSE --- LICENSE | 201 -------------------------------------------------------- 1 file changed, 201 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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 b337be23fa948958025bfb6f1d73272186be081e Mon Sep 17 00:00:00 2001 From: abdullah-cognite <100700755+abdullah-cognite@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:15:31 +0200 Subject: [PATCH 2/2] add files --- .github/renovate.json | 7 + .github/workflows/build-and-test.yml | 37 + .github/workflows/build-installer.yml | 86 ++ .github/workflows/common.yml | 130 ++ .gitignore | 15 + CODEOWNERS | 1 + Connector/Connector.csproj | 30 + Connector/ConnectorRuntime.cs | 45 + Connector/Dwsim/AutomationConfig.cs | 29 + Connector/Dwsim/DwsimClient.cs | 254 ++++ Connector/Dwsim/DwsimRoutine.cs | 277 +++++ Connector/SimulatorDefinition.cs | 1649 +++++++++++++++++++++++++ Connector/config/config.example.yml | 36 + Documentation.md | 47 + Installer/Installer.wixproj | 58 + Installer/Product.wxs | 109 ++ Installer/Resources/InstBanner.bmp | Bin 0 -> 85894 bytes Installer/Resources/InstDialog.bmp | Bin 0 -> 615320 bytes Installer/Resources/License.rtf | Bin 0 -> 495 bytes Installer/Resources/black16x16.ico | Bin 0 -> 1150 bytes Installer/Resources/black32x32.ico | Bin 0 -> 5430 bytes Installer/build.ps1 | 41 + Installer/setup-config.json | 13 + LICENSE | 201 +++ README.md | 55 + Service/Program.cs | 86 ++ Service/Service.csproj | 26 + Service/Worker.cs | 71 ++ dwsim-connector-dotnet.sln | 28 + logo.png | Bin 0 -> 3923 bytes manifest.yml | 120 ++ 31 files changed, 3451 insertions(+) create mode 100644 .github/renovate.json create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/build-installer.yml create mode 100644 .github/workflows/common.yml create mode 100644 .gitignore create mode 100644 CODEOWNERS create mode 100644 Connector/Connector.csproj create mode 100644 Connector/ConnectorRuntime.cs create mode 100644 Connector/Dwsim/AutomationConfig.cs create mode 100644 Connector/Dwsim/DwsimClient.cs create mode 100644 Connector/Dwsim/DwsimRoutine.cs create mode 100644 Connector/SimulatorDefinition.cs create mode 100644 Connector/config/config.example.yml create mode 100644 Documentation.md create mode 100644 Installer/Installer.wixproj create mode 100644 Installer/Product.wxs create mode 100644 Installer/Resources/InstBanner.bmp create mode 100644 Installer/Resources/InstDialog.bmp create mode 100644 Installer/Resources/License.rtf create mode 100644 Installer/Resources/black16x16.ico create mode 100644 Installer/Resources/black32x32.ico create mode 100644 Installer/build.ps1 create mode 100644 Installer/setup-config.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Service/Program.cs create mode 100644 Service/Service.csproj create mode 100644 Service/Worker.cs create mode 100644 dwsim-connector-dotnet.sln create mode 100644 logo.png create mode 100644 manifest.yml diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..008e503 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>cognitedata/renovate-config", + ":automergeMinor" + ] +} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..eb26d43 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,37 @@ +name: .NET build and test + +on: + pull_request: + branches: [ main ] + +jobs: + common: + uses: ./.github/workflows/common.yml + secrets: inherit + + build-installer: + runs-on: ubuntu-latest + needs: + - common + steps: + - run: echo "Yep, the installer is built" + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/build-installer.yml b/.github/workflows/build-installer.yml new file mode 100644 index 0000000..7f31945 --- /dev/null +++ b/.github/workflows/build-installer.yml @@ -0,0 +1,86 @@ +name: Release connector + +on: + push: + branches: [ main ] + +jobs: + common: + uses: ./.github/workflows/common.yml + secrets: inherit + + publish-installer: + runs-on: windows-latest + environment: CD + if: ${{ needs.common.outputs.should-release == 0 }} + needs: + - common + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install markdown-pdf + run: npm install -g markdown-pdf@11.0.0 + + - name: Convert Markdown to PDF + run: markdown-pdf Documentation.md + + - name: download-artifact + uses: actions/download-artifact@v4 + with: + name: windows artifacts + path: ./ + + - name: create release + uses: actions/create-release@v1 + id: github-release + env: + GITHUB_TOKEN: ${{ github.token }} + with: + tag_name: ${{ needs.common.outputs.version }} + release_name: DWSIM Connector ${{ needs.common.outputs.version }} + + draft: false + prerelease: true + + - name: upload release msi + uses: actions/upload-release-asset@v1 + id: github-release-msi + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.github-release.outputs.upload_url }} + asset_path: .\DwsimConnectorInstaller-${{needs.common.outputs.version }}.msi + asset_name: DwsimConnectorInstaller-${{ needs.common.outputs.version }}.msi + asset_content_type: application/octet-stream + + - name: upload release binary + uses: actions/upload-release-asset@v1 + id: github-release-binary + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.github-release.outputs.upload_url }} + asset_path: .\Service.exe + asset_name: Service.exe + asset_content_type: application/octet-stream + + - name: Install publisher + shell: bash + run: pip install cognite-extractor-publisher --extra-index-url "https://${{ secrets.ARTIFACTORY_READONLY_TOKEN_USER }}:${{ secrets.ARTIFACTORY_READONLY_TOKEN }}@cognite.jfrog.io/cognite/api/pypi/snakepit/simple" + + - name: publish connector + env: + EXTRACTOR_DOWNLOAD_API_ADMIN_SECRET: ${{ secrets.EXTRACTOR_DOWNLOAD_ADMIN_SECRET }} + run: publish-extractor publish --manifest manifest.yml --version ${{ needs.common.outputs.version }} diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml new file mode 100644 index 0000000..05aa3fd --- /dev/null +++ b/.github/workflows/common.yml @@ -0,0 +1,130 @@ +name: Build and test connector + +on: + workflow_call: + outputs: + version: + description: Release version + value: ${{ jobs.prerequisites.outputs.version }} + should-release: + description: Checks if release would occur + value: ${{ jobs.prerequisites.outputs.should-release }} + branch: + description: Branch + value: ${{ jobs.prerequisites.outputs.branch }} + secrets: + ARTIFACTORY_READONLY_TOKEN: + required: true + ARTIFACTORY_READONLY_TOKEN_USER: + required: true + +jobs: + prerequisites: + runs-on: ubuntu-latest + outputs: + should-release: ${{ steps.confirm-release.outputs.test }} + branch: ${{ steps.current-branch.outputs.branch }} + version: ${{ steps.get-version.outputs.version }} + steps: + - name: checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: update pip + run: pip install --upgrade pip + + - name: install shyaml + run: pip install shyaml + + - name: get-version + id: get-version + run: echo "version=$(cat manifest.yml | shyaml keys-0 versions | xargs -0 | cut -d\ -f1)" >> "$GITHUB_OUTPUT" + + - name: debug-version + run: echo ${{ steps.get-version.outputs.version }} + + - name: confirm release + id: confirm-release + run: echo "test=$(git tag --list '${{ steps.get-version.outputs.version }}' | wc -l | sed s/\ //g)" >> $GITHUB_OUTPUT + + - name: Get branch name + id: current-branch + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT" + + - name: Message about build + uses: actions/github-script@v7 + with: + script: | + if (${{ steps.confirm-release.outputs.test }} == 0) { + core.notice('Will release version ${{ steps.get-version.outputs.version }}...') + } else { + core.warning('Will not create release for version ${{ steps.get-version.outputs.version }} because it already exists.') + } + + build-installer: + runs-on: windows-latest + environment: ${{ needs.prerequisites.outputs.branch == 'main' && 'CD' || 'CI' }} + needs: + - prerequisites + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x + + - name: Restore tools + run: dotnet tool restore + + - name: Add msbuild to path + uses: microsoft/setup-msbuild@v1.3 + + - name: Build and Publish Service binary + working-directory: .\Service + run: dotnet publish -c Release -r win-x64 --no-self-contained -p:PublishSingleFile=true -p:Version=${{ needs.prerequisites.outputs.version }} -p:InformationalVersion="${{ needs.prerequisites.outputs.version }}" -p:DebugType=none -p:DebugSymbols=false -o ./bin/portable + shell: bash + + - name: Sign service binary + if: ${{ needs.prerequisites.outputs.branch == 'main' }} + env: + CERTIFICATE_HOST: ${{ secrets.CODE_SIGNING_CERT_HOST }} + CERTIFICATE_HOST_API_KEY: ${{ secrets.CODE_SIGNING_CERT_HOST_API_KEY }} + CERTIFICATE_SHA1_HASH: ${{ secrets.CODE_SIGNING_CERT_SHA1_HASH }} + CLIENT_CERTIFICATE: ${{ secrets.CODE_SIGNING_CLIENT_CERT }} + CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGNING_CLIENT_CERT_PASSWORD }} + KEYPAIR_ALIAS: ${{ secrets.CODE_SIGNING_KEYPAIR_ALIAS }} + uses: cognitedata/code-sign-action/@v3 + with: + path-to-binary: .\Service\bin\portable\Service.exe + + - name: Build Installer + working-directory: .\Installer + run: .\build.ps1 -b msbuild -v ${{ needs.prerequisites.outputs.version }} -d "DWSIM connector Installer" -c .\setup-config.json + shell: powershell + + - name: Sign Installer + if: ${{ needs.prerequisites.outputs.branch == 'main' }} + env: + CERTIFICATE_HOST: ${{ secrets.CODE_SIGNING_CERT_HOST }} + CERTIFICATE_HOST_API_KEY: ${{ secrets.CODE_SIGNING_CERT_HOST_API_KEY }} + CERTIFICATE_SHA1_HASH: ${{ secrets.CODE_SIGNING_CERT_SHA1_HASH }} + CLIENT_CERTIFICATE: ${{ secrets.CODE_SIGNING_CLIENT_CERT }} + CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGNING_CLIENT_CERT_PASSWORD }} + KEYPAIR_ALIAS: ${{ secrets.CODE_SIGNING_KEYPAIR_ALIAS }} + uses: cognitedata/code-sign-action/@v3 + with: + path-to-binary: '.\Installer\bin\Release\DwsimConnectorInstaller-${{ needs.prerequisites.outputs.version }}.msi' + + - run: mkdir .\uploads + - run: mv .\Installer\bin\Release\DwsimConnectorInstaller-${{ needs.prerequisites.outputs.version }}.msi .\uploads\DwsimConnectorInstaller-${{ needs.prerequisites.outputs.version }}.msi + - run: mv .\Service\bin\portable\Service.exe .\uploads\Service.exe + - name: upload artifact + if: ${{ needs.prerequisites.outputs.branch == 'main' && needs.prerequisites.outputs.should-release == 0 }} + uses: actions/upload-artifact@v4 + with: + name: windows artifacts + path: .\uploads\ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22a38a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*/obj/* +*/bin/* +config.yml +.envrc +.env +.DS_Store +files/* +logs/* +*.db +.vscode/* +configurations/* +.vs/* +*/buildoutput/* +Service/configurations/* +Service/files/* diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..f5d624f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +@cognitedata/simulator-integration diff --git a/Connector/Connector.csproj b/Connector/Connector.csproj new file mode 100644 index 0000000..0b29f73 --- /dev/null +++ b/Connector/Connector.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + false + + + + + + + + + + + + diff --git a/Connector/ConnectorRuntime.cs b/Connector/ConnectorRuntime.cs new file mode 100644 index 0000000..2810870 --- /dev/null +++ b/Connector/ConnectorRuntime.cs @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Cognite.Simulator.Utils; +using CogniteSdk.Alpha; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Connector; +public static class ConnectorRuntime { + + public static void Init() { + DefaultConnectorRuntime.ConfigureServices = ConfigureServices; + DefaultConnectorRuntime.ConnectorName = "DWSIM"; + DefaultConnectorRuntime.SimulatorDefinition = SimulatorDefinition.Get(); + } + static void ConfigureServices(IServiceCollection services) + { + services.AddScoped, DwsimClient>(); + } + + public static async Task RunStandalone() { + Init(); + await DefaultConnectorRuntime.RunStandalone().ConfigureAwait(false); + } + + public static async Task Run(ILogger defaultLogger, CancellationToken token) { + Init(); + await DefaultConnectorRuntime.Run(defaultLogger, token).ConfigureAwait(false); + } +} + diff --git a/Connector/Dwsim/AutomationConfig.cs b/Connector/Dwsim/AutomationConfig.cs new file mode 100644 index 0000000..a595bc5 --- /dev/null +++ b/Connector/Dwsim/AutomationConfig.cs @@ -0,0 +1,29 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Cognite.Simulator.Utils.Automation; + +namespace Connector; + +public class DwsimAutomationConfig : AutomationConfig +{ + public string DwsimInstallationPath { get; set; } + + public DwsimAutomationConfig() + { + ProgramId = "DWSIM.Automation.Automation3"; + } +} diff --git a/Connector/Dwsim/DwsimClient.cs b/Connector/Dwsim/DwsimClient.cs new file mode 100644 index 0000000..382ca16 --- /dev/null +++ b/Connector/Dwsim/DwsimClient.cs @@ -0,0 +1,254 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Cognite.Simulator.Utils; +using Cognite.Simulator.Utils.Automation; +using CogniteSdk.Alpha; +using Microsoft.Extensions.Logging; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +namespace Connector +{ + /// + /// Wrapper class that + /// + public class DwsimClient : + AutomationClient, + ISimulatorClient + { + private readonly ILogger _logger; + public string Version { get; init; } + private readonly Dictionary _propMap; + private readonly UnitConverter _unitConverter; + + // Lock to prevent concurrent access to simulator resources + private readonly object simulatorLock = new object(); + + public DwsimClient( + ILogger logger, DefaultConfig config) + : base(logger, config.Automation) + { + _logger = logger; + string dllPath = Path.Combine(config.Automation.DwsimInstallationPath, "DWSIM.Automation.dll"); + FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(dllPath); + Version = fvi.FileVersion != null ? fvi.FileVersion : "N/A"; + + string propsDll = Path.Combine(config.Automation.DwsimInstallationPath, "DWSIM.FlowsheetBase.dll"); + Assembly assembly = Assembly.LoadFrom(propsDll); + ResourceManager rm = new ResourceManager("DWSIM.FlowsheetBase.Properties", assembly); + _propMap = rm.GetResourceSet(CultureInfo.InvariantCulture, true, false) + .Cast().ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()); + _unitConverter = new UnitConverter(config.Automation.DwsimInstallationPath); + } + + protected override void PreShutdown() + { + if (Server != null) + { + Server.ReleaseResources(); // Clean up used DWSIM resources + _logger.LogInformation("DWSIM resources released"); + } + } + + public void TestConnection() + { + _logger.LogInformation("Testing DWSIM connection..."); + + lock (simulatorLock) + { + try + { + Initialize(); + Shutdown(); + } + catch (Exception e) + { + throw new SimulatorConnectionException($"DWSIM is not available: {e.Message}", e); + } + } + _logger.LogInformation( + "Connection to DWSIM established and removed successfully"); + } + + public bool CanOpenModel(string path) + { + _logger.LogDebug("Attempting to open file {Path}", path); + lock (simulatorLock) + { + try + { + Initialize(); + return OpenModel(path) != null; + } + catch (Exception e) when (e is COMException || e is SystemException || e is FileNotFoundException) + { + // Assuming that these are the cases that cannot be retried + throw new DwsimException(e.Message, false); + } + finally + { + Shutdown(); + } + } + } + + public dynamic OpenModel(string path) + { + return Server.LoadFlowsheet(path); + } + + public Task> RunSimulation(DefaultModelFilestate modelState, SimulatorRoutineRevision routineRev, Dictionary inputData) + { + _logger.LogDebug($"- Started running {routineRev.ExternalId} in DWSIM"); + lock (simulatorLock) + { + try + { + Initialize(); + var model = OpenModel(modelState.FilePath); + _logger.LogDebug("- Model revision {ExternalId} open in DWSIM", modelState.ExternalId); + + Dictionary result = new Dictionary(); + + var routine = new DwsimRoutine(routineRev, model, Server, inputData, _propMap, _unitConverter); + + result = routine.PerformSimulation(); + return Task.FromResult(result); + } + finally + { + Shutdown(); + } + } + } + + public Task ExtractModelInformation(DefaultModelFilestate state, CancellationToken _token) + { + try + { + bool canRead = CanOpenModel(state.FilePath); + state.CanRead = canRead; + _logger.LogInformation("{Result} model revision {ExternalId} in DWSIM", canRead ? "Successfully opened" : "Failed to open", state.ExternalId); + if (canRead) + { + state.ParsingInfo.SetSuccess(); + } + else + { + state.ParsingInfo.SetFailure(); + } + } + catch (Exception e) + { + _logger.LogError("Error while opening revision {ExternalId}: {Error}", state.ExternalId, e.Message); + state.ParsingInfo.SetFailure(); + state.CanRead = false; + } + return Task.CompletedTask; + } + + public string GetSimulatorVersion() + { + return Version; + } + + public string GetConnectorVersion() + { + return Cognite.Extractor.Metrics.Version.GetVersion( + Assembly.GetExecutingAssembly(), + "0.0.1"); + } + } + + public class UnitConverter + { + private readonly Assembly _assembly; + private readonly Type _type; + public UnitConverter(string installationPath) + { + string dllPath = Path.Combine(installationPath, "DWSIM.SharedClasses.dll"); + _assembly = Assembly.LoadFrom(dllPath); + if (_assembly == null) + { + throw new DwsimException("Failed to load System of Units assembly"); + } + else + { + var assemblyType = _assembly.GetType("DWSIM.SharedClasses.SystemsOfUnits.Converter"); + if (assemblyType == null) + { + throw new DwsimException("Cannot find DWSIM Unit Converter class type"); + } + _type = assemblyType; + } + } + + public double ConvertFromSI(string unit, double value) + { + return InvokeConversion("ConvertFromSI", unit, value); + } + + public double ConvertToSI(string unit, double value) + { + return InvokeConversion("ConvertToSI", unit, value); + } + + public double InvokeConversion(string method, string unit, double value) + { + var info = _type.GetMethod(method); + if (info == null) + { + throw new DwsimException("Cannot find unit conversion method in the DWSIM class type"); + } + var res = info.Invoke(null, new object[] { unit, value }); + if (res == null) + { + throw new DwsimException("Failed to invoke unit convertion method"); + } + return (double)res; + } + } + + public class DwsimException : Exception + { + public bool CanRetry { get; } + public DwsimException(string message, bool canRetry = true) : base(message) + { + CanRetry = canRetry; + } + + } + + public class SimulatorConnectionException : Exception + { + public SimulatorConnectionException() + { + } + + public SimulatorConnectionException(string message) : base(message) + { + } + + public SimulatorConnectionException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/Connector/Dwsim/DwsimRoutine.cs b/Connector/Dwsim/DwsimRoutine.cs new file mode 100644 index 0000000..fc88f04 --- /dev/null +++ b/Connector/Dwsim/DwsimRoutine.cs @@ -0,0 +1,277 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Cognite.Simulator.Utils; +using CogniteSdk.Alpha; + +namespace Connector; + +internal class DwsimRoutine : RoutineImplementationBase +{ + private readonly dynamic _interface; + private readonly dynamic _model; + private readonly Dictionary _propMap; + private readonly UnitConverter _units; + + + public DwsimRoutine( + SimulatorRoutineRevision routineRevision, + dynamic model, + dynamic interf, + Dictionary inputData, + Dictionary propMap, + UnitConverter units) : + base(routineRevision, inputData) + { + _model = model; + _interface = interf; + _propMap = propMap; + _units = units; + } + + public override void SetInput(SimulatorRoutineRevisionInput inputConfig, SimulatorValueItem input, Dictionary arguments) + { + var (objectName, objectProperty) = GetObjectNameAndProperty(arguments); + SetPropertyValue(objectProperty, objectName, input); + input.SimulatorObjectReference = new Dictionary { + { "objectName", objectName }, + { "objectProperty", objectProperty }, + }; + } + + private SimulatorValue GetCompositionValue(dynamic outObj, SimulatorRoutineRevisionOutput outputConfig, string objectName) + { + if (outputConfig.ValueType != SimulatorValueType.DOUBLE_ARRAY) + { + throw new SimulationException($"Get error: Unsupported value type {outputConfig.ValueType} for property 'Composition'. Expected DOUBLE_ARRAY"); + } + + try { + double[] composition = outObj.GetOverallComposition(); + return SimulatorValue.Create(composition); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + throw new SimulationException($"Get error: Unrecognized property 'Composition' for object {objectName} {e}"); + } + } + + private SimulatorValue GetComponentsValue(dynamic outObj, SimulatorRoutineRevisionOutput outputConfig, string objectName) + { + if (outputConfig.ValueType != SimulatorValueType.STRING_ARRAY) + { + throw new SimulationException($"Get error: Unsupported value type {outputConfig.ValueType} for property 'Components'. Expected STRING_ARRAY"); + } + + try { + string[] components = outObj.ComponentIds; + return SimulatorValue.Create(components); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + throw new SimulationException($"Get error: Unrecognized property 'Components' for object {objectName} {e}"); + } + } + + + public override SimulatorValueItem GetOutput(SimulatorRoutineRevisionOutput outputConfig, Dictionary arguments) + { + var (objectName, objectProperty) = GetObjectNameAndProperty(arguments); + + dynamic outObj = _model.GetFlowsheetSimulationObject(objectName); + if (outObj == null) + { + throw new SimulationException($"Get error: Cannot find flowsheet object named '{objectName}'"); + } + + var resultItem = new SimulatorValueItem() + { + SimulatorObjectReference = new Dictionary { + { "objectName", objectName }, + { "objectProperty", objectProperty }, + }, + TimeseriesExternalId = outputConfig.SaveTimeseriesExternalId, + ReferenceId = outputConfig.ReferenceId, + ValueType = outputConfig.ValueType, + }; + + // Special logic for "Composition" property + if (objectProperty == "Composition") + { + resultItem.Value = GetCompositionValue(outObj, outputConfig, objectName);; + return resultItem; + } + + // Special logic for "Components" property + if (objectProperty == "Components") + { + resultItem.Value = GetComponentsValue(outObj, outputConfig, objectName); + return resultItem; + } + + // Properties that we can read from + string[] outObjProps = outObj.GetProperties(3); + string propKey = objectProperty; + + // Some properties have different names in the UI and in the simulation model + if (!outObjProps.Contains(objectProperty)) + { + // translate from UI label to property ID. + var props = _propMap.Where(kvp => kvp.Value == objectProperty && outObjProps.Contains(kvp.Key)); + if (!props.Any()) + { + throw new SimulationException($"Get error: Unrecognized property '{objectProperty}'"); + } + propKey = props.First().Key; + } + var value = outObj.GetPropertyValue(propKey); + SimulatorValue resultValue; + SimulatorValueUnit? outputUnit = null; + if (outputConfig.ValueType == SimulatorValueType.DOUBLE && value is double) + { + if (outputConfig.Unit?.Name != null) + { + double result = _units.ConvertFromSI(outputConfig.Unit.Name, value); + resultValue = SimulatorValue.Create(result); + outputUnit = new SimulatorValueUnit() + { + Name = outputConfig.Unit?.Name, + }; + } + else + { + resultValue = SimulatorValue.Create(value); + } + } + else if (outputConfig.ValueType == SimulatorValueType.STRING) + { + resultValue = SimulatorValue.Create(value.ToString()); + } + else + { + throw new SimulationException($"Get error: Unsupported value type {outputConfig.ValueType} for property {objectProperty} of object {objectName} with value {value}"); + } + + resultItem.Value = resultValue; + resultItem.Unit = outputUnit; + return resultItem; + } + + public override void RunCommand(Dictionary arguments) + { + if (!arguments.TryGetValue("command", out string? command)) + { + throw new SimulationException($"Command error: Command not defined"); + } + if (command == "Solve") + { + _interface.CalculateFlowsheet2(_model); + if (!_model.Solved) + { + throw new SimulationException($"Command error: {_model.ErrorMessage}"); + } + } + else + { + throw new SimulationException($"Command error: Invalid command type {command}"); + } + } + + private void SetCompositionValue(dynamic inObj, SimulatorValueItem valueItem, string objectName) + { + if (valueItem.ValueType == SimulatorValueType.DOUBLE_ARRAY) + { + var arrValue = ((SimulatorValue.DoubleArray) valueItem.Value)?.Value; + try + { + inObj.SetOverallMolarComposition(arrValue?.ToArray()); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) + { + throw new SimulationException($"Set error: Unrecognized property 'Composition' for object {objectName} {e}"); + } + } + else + { + throw new SimulationException($"Set error: Unsupported value type {valueItem.ValueType} for property 'Composition'"); + } + } + + private void SetPropertyValue(string propertyName, string objectName, SimulatorValueItem valueItem) + { + dynamic inObj = _model.GetFlowsheetSimulationObject(objectName); + if (inObj == null) + { + throw new SimulationException($"Set error: Cannot find flowsheet object named '{objectName}'"); + } + + // Special logic for "Composition" property + if (propertyName == "Composition") + { + SetCompositionValue(inObj, valueItem, objectName); + return; + } + + // Properties that we can write to + string[] inObjProps = inObj.GetProperties(1); + string propKey = propertyName; + if (!inObjProps.Contains(propertyName)) + { + // translate from UI label to property ID. + var props = _propMap.Where(kvp => kvp.Value == propertyName && inObjProps.Contains(kvp.Key)); + if (!props.Any()) + { + throw new SimulationException($"Set error: Unrecognized property '{propertyName}' for object '{objectName}'"); + } + propKey = props.First().Key; + } + bool success = false; + + if (valueItem.ValueType == SimulatorValueType.DOUBLE) + { + var rawValue = ((SimulatorValue.Double) valueItem.Value).Value; + double value = rawValue; + if (valueItem.Unit?.Name != null) + { + value = _units.ConvertToSI(valueItem.Unit.Name, rawValue); + } + success = inObj.SetPropertyValue(propKey, value); + } + else if (valueItem.ValueType == SimulatorValueType.STRING) + { + var rawValue = ((SimulatorValue.String) valueItem.Value).Value; + success = inObj.SetPropertyValue(propKey, rawValue); + } + if (!success) + { + throw new SimulationException($"Set error: Failed to set property {propertyName} ({propKey}) to {valueItem} unit: {valueItem?.Unit?.Name}"); + } + } + + private (string Name, string Property) GetObjectNameAndProperty(Dictionary arguments) + { + if (!arguments.TryGetValue("objectProperty", out string objectProperty)) + { + throw new SimulationException($"Set error: Object property not defined"); + } + if (!arguments.TryGetValue("objectName", out string objectName)) + { + throw new SimulationException($"Set error: Object name not defined"); + } + + return (objectName, objectProperty); + } +} \ No newline at end of file diff --git a/Connector/SimulatorDefinition.cs b/Connector/SimulatorDefinition.cs new file mode 100644 index 0000000..98f2bd5 --- /dev/null +++ b/Connector/SimulatorDefinition.cs @@ -0,0 +1,1649 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using CogniteSdk.Alpha; +using System.Collections.Generic; + +namespace Connector +{ + static class SimulatorDefinition + { + public static SimulatorCreate Get() + { + return new SimulatorCreate() + { + ExternalId = "DWSIM", + Name = "DWSIM", + FileExtensionTypes = new List { "dwxmz" }, + ModelTypes = new List { + new SimulatorModelType { + Name = "Steady State", + Key = "SteadyState", + } + }, + StepFields = new List { + new SimulatorStepField { + StepType = "get/set", + Fields = new List { + new SimulatorStepFieldParam { + Name = "objectName", + Label = "Simulation Object Name", + Info = "Enter the name of the DWSIM object, i.e. Feed", + }, + new SimulatorStepFieldParam { + Name = "objectProperty", + Label = "Simulation Object Property", + Info = "Enter the property of the DWSIM object, i.e. Temperature", + }, + }, + }, + new SimulatorStepField { + StepType = "command", + Fields = new List { + new SimulatorStepFieldParam { + Name = "command", + Label = "Command", + Info = "Select a command", + Options = new List { + new SimulatorStepFieldOption { + Label = "Solve Flowsheet", + Value = "Solve", + } + }, + }, + }, + }, + }, + UnitQuantities = new List { + new SimulatorUnitQuantity { + Label = "Acceleration", + Name = "accel", + Units = new List { + new SimulatorUnitEntry { + Label = "m/s²", + Name = "m/s2", + }, + new SimulatorUnitEntry { + Label = "cm/s²", + Name = "cm/s2", + }, + new SimulatorUnitEntry { + Label = "ft/s²", + Name = "ft/s2", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Activity", + Name = "activity", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Activity Coefficient", + Name = "activityCoefficient", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Area", + Name = "area", + Units = new List { + new SimulatorUnitEntry { + Label = "m²", + Name = "m2", + }, + new SimulatorUnitEntry { + Label = "cm²", + Name = "cm2", + }, + new SimulatorUnitEntry { + Label = "ft²", + Name = "ft2", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Boiling Point Temperature", + Name = "boilingPointTemperature", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Cake Resistance", + Name = "cakeresistance", + Units = new List { + new SimulatorUnitEntry { + Label = "m/kg", + Name = "m/kg", + }, + new SimulatorUnitEntry { + Label = "ft/lbm", + Name = "ft/lbm", + }, + new SimulatorUnitEntry { + Label = "cm/g", + Name = "cm/g", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Kinematic Viscosity", + Name = "cinematic_viscosity", + Units = new List { + new SimulatorUnitEntry { + Label = "m²/s", + Name = "m2/s", + }, + new SimulatorUnitEntry { + Label = "cSt", + Name = "cSt", + }, + new SimulatorUnitEntry { + Label = "ft²/s", + Name = "ft2/s", + }, + new SimulatorUnitEntry { + Label = "mm²/s", + Name = "mm2/s", + }, + new SimulatorUnitEntry { + Label = "cm²/s", + Name = "cm2/s", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Compressibility", + Name = "compressibility", + Units = new List { + new SimulatorUnitEntry { + Label = "1/Pa", + Name = "1/Pa", + }, + new SimulatorUnitEntry { + Label = "1/atm", + Name = "1/atm", + }, + new SimulatorUnitEntry { + Label = "1/kPa", + Name = "1/kPa", + }, + new SimulatorUnitEntry { + Label = "1/bar", + Name = "1/bar", + }, + new SimulatorUnitEntry { + Label = "1/MPa", + Name = "1/MPa", + }, + new SimulatorUnitEntry { + Label = "1/psi", + Name = "1/psi", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Compressibility Factor", + Name = "compressibilityFactor", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Conductance", + Name = "conductance", + Units = new List { + new SimulatorUnitEntry { + Label = "[kg/s]/[Pa^0.5]", + Name = "[kg/s]/[Pa^0.5]", + }, + new SimulatorUnitEntry { + Label = "[lbm/h]/[psi^0.5]", + Name = "[lbm/h]/[psi^0.5]", + }, + new SimulatorUnitEntry { + Label = "[kg/h]/[atm^0.5]", + Name = "[kg/h]/[atm^0.5]", + }, + new SimulatorUnitEntry { + Label = "[kg/h]/[bar^0.5]", + Name = "[kg/h]/[bar^0.5]", + }, + new SimulatorUnitEntry { + Label = "[kg/h]/[[kgf/cm²]^0.5]", + Name = "[kg/h]/[[kgf/cm2]^0.5]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Delta P", + Name = "deltaP", + Units = new List { + new SimulatorUnitEntry { + Label = "Pa", + Name = "Pa", + }, + new SimulatorUnitEntry { + Label = "atm", + Name = "atm", + }, + new SimulatorUnitEntry { + Label = "lbf/ft²", + Name = "lbf/ft2", + }, + new SimulatorUnitEntry { + Label = "kgf/cm²", + Name = "kgf/cm2", + }, + new SimulatorUnitEntry { + Label = "kgf/cm²(g)", + Name = "kgf/cm2_g", + }, + new SimulatorUnitEntry { + Label = "kPa", + Name = "kPa", + }, + new SimulatorUnitEntry { + Label = "bar", + Name = "bar", + }, + new SimulatorUnitEntry { + Label = "bar(g)", + Name = "barg", + }, + new SimulatorUnitEntry { + Label = "ftH₂O", + Name = "ftH2O", + }, + new SimulatorUnitEntry { + Label = "inH₂O", + Name = "inH2O", + }, + new SimulatorUnitEntry { + Label = "inHg", + Name = "inHg", + }, + new SimulatorUnitEntry { + Label = "mbar", + Name = "mbar", + }, + new SimulatorUnitEntry { + Label = "mH₂O", + Name = "mH2O", + }, + new SimulatorUnitEntry { + Label = "mmH₂O", + Name = "mmH2O", + }, + new SimulatorUnitEntry { + Label = "mmHg", + Name = "mmHg", + }, + new SimulatorUnitEntry { + Label = "MPa", + Name = "MPa", + }, + new SimulatorUnitEntry { + Label = "psi", + Name = "psi", + }, + new SimulatorUnitEntry { + Label = "psi(g)", + Name = "psig", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Delta T", + Name = "deltaT", + Units = new List { + new SimulatorUnitEntry { + Label = "°C", + Name = "C.", + }, + new SimulatorUnitEntry { + Label = "K", + Name = "K.", + }, + new SimulatorUnitEntry { + Label = "°F", + Name = "F.", + }, + new SimulatorUnitEntry { + Label = "°R", + Name = "R.", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Density", + Name = "density", + Units = new List { + new SimulatorUnitEntry { + Label = "kg/m³", + Name = "kg/m3", + }, + new SimulatorUnitEntry { + Label = "g/cm³", + Name = "g/cm3", + }, + new SimulatorUnitEntry { + Label = "lbm/ft³", + Name = "lbm/ft3", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Diameter", + Name = "diameter", + Units = new List { + new SimulatorUnitEntry { + Label = "mm", + Name = "mm", + }, + new SimulatorUnitEntry { + Label = "in", + Name = "in", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Diffusivity", + Name = "diffusivity", + Units = new List { + new SimulatorUnitEntry { + Label = "m²/s", + Name = "m2/s", + }, + new SimulatorUnitEntry { + Label = "cSt", + Name = "cSt", + }, + new SimulatorUnitEntry { + Label = "ft²/s", + Name = "ft2/s", + }, + new SimulatorUnitEntry { + Label = "mm²/s", + Name = "mm2/s", + }, + new SimulatorUnitEntry { + Label = "cm²/s", + Name = "cm2/s", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Distance", + Name = "distance", + Units = new List { + new SimulatorUnitEntry { + Label = "m", + Name = "m", + }, + new SimulatorUnitEntry { + Label = "ft", + Name = "ft", + }, + new SimulatorUnitEntry { + Label = "cm", + Name = "cm", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Enthalpy", + Name = "enthalpy", + Units = new List { + new SimulatorUnitEntry { + Label = "kJ/kg", + Name = "kJ/kg", + }, + new SimulatorUnitEntry { + Label = "cal/g", + Name = "cal/g", + }, + new SimulatorUnitEntry { + Label = "BTU/lbm", + Name = "BTU/lbm", + }, + new SimulatorUnitEntry { + Label = "kcal/kg", + Name = "kcal/kg", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Entropy", + Name = "entropy", + Units = new List { + new SimulatorUnitEntry { + Label = "kJ/[kg.K]", + Name = "kJ/[kg.K]", + }, + new SimulatorUnitEntry { + Label = "cal/[g.C]", + Name = "cal/[g.C]", + }, + new SimulatorUnitEntry { + Label = "BTU/[lbm.R]", + Name = "BTU/[lbm.R]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Excess Enthalpy", + Name = "excessEnthalpy", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Excess Entropy", + Name = "excessEntropy", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Force", + Name = "force", + Units = new List { + new SimulatorUnitEntry { + Label = "N", + Name = "N", + }, + new SimulatorUnitEntry { + Label = "dyn", + Name = "dyn", + }, + new SimulatorUnitEntry { + Label = "kgf", + Name = "kgf", + }, + new SimulatorUnitEntry { + Label = "lbf", + Name = "lbf", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Fouling Factor", + Name = "foulingfactor", + Units = new List { + new SimulatorUnitEntry { + Label = "K.m²/W", + Name = "K.m2/W", + }, + new SimulatorUnitEntry { + Label = "C.cm².s/cal", + Name = "C.cm2.s/cal", + }, + new SimulatorUnitEntry { + Label = "ft².h.F/BTU", + Name = "ft2.h.F/BTU", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Fugacity", + Name = "fugacity", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Fugacity Coefficient", + Name = "fugacityCoefficient", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "GOR", + Name = "gor", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Head", + Name = "head", + Units = new List { + new SimulatorUnitEntry { + Label = "m", + Name = "m", + }, + new SimulatorUnitEntry { + Label = "ft", + Name = "ft", + }, + new SimulatorUnitEntry { + Label = "cm", + Name = "cm", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Cp", + Name = "heatCapacityCp", + Units = new List { + new SimulatorUnitEntry { + Label = "kJ/[kg.K]", + Name = "kJ/[kg.K]", + }, + new SimulatorUnitEntry { + Label = "cal/[g.C]", + Name = "cal/[g.C]", + }, + new SimulatorUnitEntry { + Label = "BTU/[lbm.R]", + Name = "BTU/[lbm.R]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Cv", + Name = "heatCapacityCv", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Heat Transfer Coefficient", + Name = "heat_transf_coeff", + Units = new List { + new SimulatorUnitEntry { + Label = "W/[m².K]", + Name = "W/[m2.K]", + }, + new SimulatorUnitEntry { + Label = "cal/[cm².s.C]", + Name = "cal/[cm2.s.C]", + }, + new SimulatorUnitEntry { + Label = "BTU/[ft².h.R]", + Name = "BTU/[ft2.h.R]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Heat Flow", + Name = "heatflow", + Units = new List { + new SimulatorUnitEntry { + Label = "kW", + Name = "kW", + }, + new SimulatorUnitEntry { + Label = "kcal/h", + Name = "kcal/h", + }, + new SimulatorUnitEntry { + Label = "BTU/h", + Name = "BTU/h", + }, + new SimulatorUnitEntry { + Label = "BTU/s", + Name = "BTU/s", + }, + new SimulatorUnitEntry { + Label = "cal/s", + Name = "cal/s", + }, + new SimulatorUnitEntry { + Label = "HP", + Name = "HP", + }, + new SimulatorUnitEntry { + Label = "kJ/h", + Name = "kJ/h", + }, + new SimulatorUnitEntry { + Label = "kJ/d", + Name = "kJ/d", + }, + new SimulatorUnitEntry { + Label = "MW", + Name = "MW", + }, + new SimulatorUnitEntry { + Label = "W", + Name = "W", + }, + new SimulatorUnitEntry { + Label = "BTU/d", + Name = "BTU/d", + }, + new SimulatorUnitEntry { + Label = "MMBTU/d", + Name = "MMBTU/d", + }, + new SimulatorUnitEntry { + Label = "MMBTU/h", + Name = "MMBTU/h", + }, + new SimulatorUnitEntry { + Label = "kcal/s", + Name = "kcal/s", + }, + new SimulatorUnitEntry { + Label = "kcal/h", + Name = "kcal/h", + }, + new SimulatorUnitEntry { + Label = "kcal/d", + Name = "kcal/d", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Ideal Gas Heat Capacity", + Name = "idealGasHeatCapacity", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Joule Thomson Coefficient", + Name = "jouleThomsonCoefficient", + Units = new List { + new SimulatorUnitEntry { + Label = "K/Pa", + Name = "K/Pa", + }, + new SimulatorUnitEntry { + Label = "°F/psi", + Name = "F/psi", + }, + new SimulatorUnitEntry { + Label = "°C/atm", + Name = "C/atm", + }, + } + }, + new SimulatorUnitQuantity { + Label = "K Value", + Name = "kvalue", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "log Fugacity Coefficient", + Name = "logFugacityCoefficient", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "log K Value", + Name = "logKvalue", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Mass", + Name = "mass", + Units = new List { + new SimulatorUnitEntry { + Label = "kg", + Name = "kg", + }, + new SimulatorUnitEntry { + Label = "g", + Name = "g", + }, + new SimulatorUnitEntry { + Label = "lb", + Name = "lb", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Mass Concentration", + Name = "mass_conc", + Units = new List { + new SimulatorUnitEntry { + Label = "kg/m³", + Name = "kg/m3", + }, + new SimulatorUnitEntry { + Label = "g/L", + Name = "g/L", + }, + new SimulatorUnitEntry { + Label = "g/cm³", + Name = "g/cm3", + }, + new SimulatorUnitEntry { + Label = "g/mL", + Name = "g/mL", + }, + new SimulatorUnitEntry { + Label = "lbm/ft³", + Name = "lbm/ft3", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Mass Flow", + Name = "massflow", + Units = new List { + new SimulatorUnitEntry { + Label = "g/s", + Name = "g/s", + }, + new SimulatorUnitEntry { + Label = "lbm/h", + Name = "lbm/h", + }, + new SimulatorUnitEntry { + Label = "kg/s", + Name = "kg/s", + }, + new SimulatorUnitEntry { + Label = "kg/h", + Name = "kg/h", + }, + new SimulatorUnitEntry { + Label = "kg/d", + Name = "kg/d", + }, + new SimulatorUnitEntry { + Label = "kg/min", + Name = "kg/min", + }, + new SimulatorUnitEntry { + Label = "lb/min", + Name = "lb/min", + }, + new SimulatorUnitEntry { + Label = "lb/s", + Name = "lb/s", + }, + new SimulatorUnitEntry { + Label = "lb/h", + Name = "lb/h", + }, + new SimulatorUnitEntry { + Label = "lb/d", + Name = "lb/d", + }, + new SimulatorUnitEntry { + Label = "Mg/s", + Name = "Mg/s", + }, + new SimulatorUnitEntry { + Label = "Mg/h", + Name = "Mg/h", + }, + new SimulatorUnitEntry { + Label = "Mg/d", + Name = "Mg/d", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Mass Fraction", + Name = "massfraction", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Medium Resistance", + Name = "mediumresistance", + Units = new List { + new SimulatorUnitEntry { + Label = "m-1", + Name = "m-1", + }, + new SimulatorUnitEntry { + Label = "cm-1", + Name = "cm-1", + }, + new SimulatorUnitEntry { + Label = "ft-1", + Name = "ft-1", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Melting Temperature", + Name = "meltingTemperature", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Concentration", + Name = "molar_conc", + Units = new List { + new SimulatorUnitEntry { + Label = "kmol/m³", + Name = "kmol/m3", + }, + new SimulatorUnitEntry { + Label = "mol/m³", + Name = "mol/m3", + }, + new SimulatorUnitEntry { + Label = "mol/L", + Name = "mol/L", + }, + new SimulatorUnitEntry { + Label = "mol/cm³", + Name = "mol/cm3", + }, + new SimulatorUnitEntry { + Label = "mol/mL", + Name = "mol/mL", + }, + new SimulatorUnitEntry { + Label = "lbmol/ft³", + Name = "lbmol/ft3", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Enthalpy", + Name = "molar_enthalpy", + Units = new List { + new SimulatorUnitEntry { + Label = "kJ/kmol", + Name = "kJ/kmol", + }, + new SimulatorUnitEntry { + Label = "cal/mol", + Name = "cal/mol", + }, + new SimulatorUnitEntry { + Label = "BTU/lbmol", + Name = "BTU/lbmol", + }, + new SimulatorUnitEntry { + Label = "J/mol", + Name = "J/mol", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Entropy", + Name = "molar_entropy", + Units = new List { + new SimulatorUnitEntry { + Label = "kJ/[kmol.K]", + Name = "kJ/[kmol.K]", + }, + new SimulatorUnitEntry { + Label = "cal/[mol.C]", + Name = "cal/[mol.C]", + }, + new SimulatorUnitEntry { + Label = "BTU/[lbmol.R]", + Name = "BTU/[lbmol.R]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Volume", + Name = "molar_volume", + Units = new List { + new SimulatorUnitEntry { + Label = "m³/kmol", + Name = "m3/kmol", + }, + new SimulatorUnitEntry { + Label = "cm³/mmol", + Name = "cm3/mmol", + }, + new SimulatorUnitEntry { + Label = "ft³/lbmol", + Name = "ft3/lbmol", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Flow", + Name = "molarflow", + Units = new List { + new SimulatorUnitEntry { + Label = "mol/s", + Name = "mol/s", + }, + new SimulatorUnitEntry { + Label = "lbmol/h", + Name = "lbmol/h", + }, + new SimulatorUnitEntry { + Label = "mol/h", + Name = "mol/h", + }, + new SimulatorUnitEntry { + Label = "mol/d", + Name = "mol/d", + }, + new SimulatorUnitEntry { + Label = "kmol/s", + Name = "kmol/s", + }, + new SimulatorUnitEntry { + Label = "kmol/h", + Name = "kmol/h", + }, + new SimulatorUnitEntry { + Label = "kmol/d", + Name = "kmol/d", + }, + new SimulatorUnitEntry { + Label = "m³/d @ BR", + Name = "m3/d @ BR", + }, + new SimulatorUnitEntry { + Label = "m³/d @ NC", + Name = "m3/d @ NC", + }, + new SimulatorUnitEntry { + Label = "m³/d @ CNTP", + Name = "m3/d @ CNTP", + }, + new SimulatorUnitEntry { + Label = "m³/d @ SC", + Name = "m3/d @ SC", + }, + new SimulatorUnitEntry { + Label = "m³/d @ 0 °C, 1 atm", + Name = "m3/d @ 0 C, 1 atm", + }, + new SimulatorUnitEntry { + Label = "m³/d @ 15.56 °C, 1 atm", + Name = "m3/d @ 15.56 C, 1 atm", + }, + new SimulatorUnitEntry { + Label = "m³/d @ 20 °C, 1 atm", + Name = "m3/d @ 20 C, 1 atm", + }, + new SimulatorUnitEntry { + Label = "ft³/d @ 60 °F, 14.7 psia", + Name = "ft3/d @ 60 f, 14.7 psia", + }, + new SimulatorUnitEntry { + Label = "ft³/d @ 0 °C, 1 atm", + Name = "ft3/d @ 0 C, 1 atm", + }, + new SimulatorUnitEntry { + Label = "MMSCFD", + Name = "MMSCFD", + }, + new SimulatorUnitEntry { + Label = "SCFD", + Name = "SCFD", + }, + new SimulatorUnitEntry { + Label = "SCFM", + Name = "SCFM", + }, + new SimulatorUnitEntry { + Label = "Mm³/d @ BR", + Name = "Mm3/d @ BR", + }, + new SimulatorUnitEntry { + Label = "Mm³/d @ SC", + Name = "Mm3/d @ SC", + }, + new SimulatorUnitEntry { + Label = "Mm³/d @ NC", + Name = "Mm3/d @ NC", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molar Fraction", + Name = "molarfraction", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Molecular Weight", + Name = "molecularWeight", + Units = new List { + new SimulatorUnitEntry { + Label = "kg/kmol", + Name = "kg/kmol", + }, + new SimulatorUnitEntry { + Label = "g/mol", + Name = "g/mol", + }, + new SimulatorUnitEntry { + Label = "lbm/lbmol", + Name = "lbm/lbmol", + }, + } + }, + new SimulatorUnitQuantity { + Label = "None", + Name = "none", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Pressure", + Name = "pressure", + Units = new List { + new SimulatorUnitEntry { + Label = "Pa", + Name = "Pa", + }, + new SimulatorUnitEntry { + Label = "atm", + Name = "atm", + }, + new SimulatorUnitEntry { + Label = "kgf/cm²", + Name = "kgf/cm2", + }, + new SimulatorUnitEntry { + Label = "kgf/cm²(g)", + Name = "kgf/cm2g", + }, + new SimulatorUnitEntry { + Label = "lbf/ft²", + Name = "lbf/ft2", + }, + new SimulatorUnitEntry { + Label = "kPa", + Name = "kPa", + }, + new SimulatorUnitEntry { + Label = "kPa(g)", + Name = "kPag", + }, + new SimulatorUnitEntry { + Label = "bar", + Name = "bar", + }, + new SimulatorUnitEntry { + Label = "bar(g)", + Name = "barg", + }, + new SimulatorUnitEntry { + Label = "ftH₂O", + Name = "ftH2O", + }, + new SimulatorUnitEntry { + Label = "inH₂O", + Name = "inH2O", + }, + new SimulatorUnitEntry { + Label = "inHg", + Name = "inHg", + }, + new SimulatorUnitEntry { + Label = "mbar", + Name = "mbar", + }, + new SimulatorUnitEntry { + Label = "mH₂O", + Name = "mH2O", + }, + new SimulatorUnitEntry { + Label = "mmH₂O", + Name = "mmH2O", + }, + new SimulatorUnitEntry { + Label = "mmHg", + Name = "mmHg", + }, + new SimulatorUnitEntry { + Label = "MPa", + Name = "MPa", + }, + new SimulatorUnitEntry { + Label = "psi", + Name = "psi", + }, + new SimulatorUnitEntry { + Label = "psi(g)", + Name = "psig", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Reaction Rate", + Name = "reac_rate", + Units = new List { + new SimulatorUnitEntry { + Label = "kmol/[m³.s]", + Name = "kmol/[m3.s]", + }, + new SimulatorUnitEntry { + Label = "kmol/[m³.min]", + Name = "kmol/[m3.min.]", + }, + new SimulatorUnitEntry { + Label = "kmol/[m³.h]", + Name = "kmol/[m3.h]", + }, + new SimulatorUnitEntry { + Label = "mol/[m³.s]", + Name = "mol/[m3.s]", + }, + new SimulatorUnitEntry { + Label = "mol/[m³.min]", + Name = "mol/[m3.min.]", + }, + new SimulatorUnitEntry { + Label = "mol/[m³.h]", + Name = "mol/[m3.h]", + }, + new SimulatorUnitEntry { + Label = "mol/[L.s]", + Name = "mol/[L.s]", + }, + new SimulatorUnitEntry { + Label = "mol/[L.min]", + Name = "mol/[L.min.]", + }, + new SimulatorUnitEntry { + Label = "mol/[L.h]", + Name = "mol/[L.h]", + }, + new SimulatorUnitEntry { + Label = "mol/[cm³.s]", + Name = "mol/[cm3.s]", + }, + new SimulatorUnitEntry { + Label = "mol/[cm³.min]", + Name = "mol/[cm3.min.]", + }, + new SimulatorUnitEntry { + Label = "mol/[cm³.h]", + Name = "mol/[cm3.h]", + }, + new SimulatorUnitEntry { + Label = "lbmol/[ft³.h]", + Name = "lbmol/[ft3.h]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Reaction Rate Heterogeneous", + Name = "reac_rate_heterog", + Units = new List { + new SimulatorUnitEntry { + Label = "kmol/[kg.s]", + Name = "kmol/[kg.s]", + }, + new SimulatorUnitEntry { + Label = "kmol/[kg.min.]", + Name = "kmol/[kg.min.]", + }, + new SimulatorUnitEntry { + Label = "kmol/[kg.h]", + Name = "kmol/[kg.h]", + }, + new SimulatorUnitEntry { + Label = "mol/[kg.s]", + Name = "mol/[kg.s]", + }, + new SimulatorUnitEntry { + Label = "mol/[kg.min.]", + Name = "mol/[kg.min.]", + }, + new SimulatorUnitEntry { + Label = "mol/[kg.h]", + Name = "mol/[kg.h]", + }, + new SimulatorUnitEntry { + Label = "lbmol/[lbm.h]", + Name = "lbmol/[lbm.h]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Specific Volume", + Name = "spec_vol", + Units = new List { + new SimulatorUnitEntry { + Label = "m³/kg", + Name = "m3/kg", + }, + new SimulatorUnitEntry { + Label = "cm³/g", + Name = "cm3/g", + }, + new SimulatorUnitEntry { + Label = "ft³/lbm", + Name = "ft3/lbm", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Speed of Sound", + Name = "speedOfSound", + Units = new List { + new SimulatorUnitEntry { + Label = "m/s", + Name = "m/s", + }, + new SimulatorUnitEntry { + Label = "cm/s", + Name = "cm/s", + }, + new SimulatorUnitEntry { + Label = "mm/s", + Name = "mm/s", + }, + new SimulatorUnitEntry { + Label = "km/h", + Name = "km/h", + }, + new SimulatorUnitEntry { + Label = "ft/h", + Name = "ft/h", + }, + new SimulatorUnitEntry { + Label = "ft/min", + Name = "ft/min", + }, + new SimulatorUnitEntry { + Label = "ft/s", + Name = "ft/s", + }, + new SimulatorUnitEntry { + Label = "in/s", + Name = "in/s", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Surface Tension", + Name = "surfaceTension", + Units = new List { + new SimulatorUnitEntry { + Label = "N/m", + Name = "N/m", + }, + new SimulatorUnitEntry { + Label = "dyn/cm", + Name = "dyn/cm", + }, + new SimulatorUnitEntry { + Label = "lbf/in", + Name = "lbf/in", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Temperature", + Name = "temperature", + Units = new List { + new SimulatorUnitEntry { + Label = "K", + Name = "K", + }, + new SimulatorUnitEntry { + Label = "°R", + Name = "R", + }, + new SimulatorUnitEntry { + Label = "°C", + Name = "C", + }, + new SimulatorUnitEntry { + Label = "°F", + Name = "F", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Thermal Conductivity", + Name = "thermalConductivity", + Units = new List { + new SimulatorUnitEntry { + Label = "W/[m.K]", + Name = "W/[m.K]", + }, + new SimulatorUnitEntry { + Label = "cal/[cm.s.C]", + Name = "cal/[cm.s.C]", + }, + new SimulatorUnitEntry { + Label = "BTU/[ft.h.R]", + Name = "BTU/[ft.h.R]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Liquid Thermal Conductivity", + Name = "thermalConductivityOfLiquid", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Vapor Thermal Conductivity", + Name = "thermalConductivityOfVapor", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Thickness", + Name = "thickness", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Time", + Name = "time", + Units = new List { + new SimulatorUnitEntry { + Label = "s", + Name = "s", + }, + new SimulatorUnitEntry { + Label = "min", + Name = "min.", + }, + new SimulatorUnitEntry { + Label = "h", + Name = "h", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Vapor Pressure", + Name = "vaporPressure", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Velocity", + Name = "velocity", + Units = new List { + new SimulatorUnitEntry { + Label = "m/s", + Name = "m/s", + }, + new SimulatorUnitEntry { + Label = "cm/s", + Name = "cm/s", + }, + new SimulatorUnitEntry { + Label = "mm/s", + Name = "mm/s", + }, + new SimulatorUnitEntry { + Label = "km/h", + Name = "km/h", + }, + new SimulatorUnitEntry { + Label = "ft/h", + Name = "ft/h", + }, + new SimulatorUnitEntry { + Label = "ft/min", + Name = "ft/min", + }, + new SimulatorUnitEntry { + Label = "ft/s", + Name = "ft/s", + }, + new SimulatorUnitEntry { + Label = "in/s", + Name = "in/s", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Viscosity", + Name = "viscosity", + Units = new List { + new SimulatorUnitEntry { + Label = "kg/[m.s]", + Name = "kg/[m.s]", + }, + new SimulatorUnitEntry { + Label = "Pa.s", + Name = "Pa.s", + }, + new SimulatorUnitEntry { + Label = "cP", + Name = "cP", + }, + new SimulatorUnitEntry { + Label = "lbm/[ft.h]", + Name = "lbm/[ft.h]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Liquid Viscosity", + Name = "viscosityOfLiquid", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Vapor Viscosity", + Name = "viscosityOfVapor", + Units = new List { + new SimulatorUnitEntry { + Label = "", + Name = "", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Volume", + Name = "volume", + Units = new List { + new SimulatorUnitEntry { + Label = "m³", + Name = "m3", + }, + new SimulatorUnitEntry { + Label = "cm³", + Name = "cm3", + }, + new SimulatorUnitEntry { + Label = "L", + Name = "L", + }, + new SimulatorUnitEntry { + Label = "ft³", + Name = "ft3", + }, + new SimulatorUnitEntry { + Label = "bbl", + Name = "bbl", + }, + new SimulatorUnitEntry { + Label = "gal[US]", + Name = "gal[US]", + }, + new SimulatorUnitEntry { + Label = "gal[UK]", + Name = "gal[UK]", + }, + } + }, + new SimulatorUnitQuantity { + Label = "Volumetric Flow", + Name = "volumetricFlow", + Units = new List { + new SimulatorUnitEntry { + Label = "m³/s", + Name = "m3/s", + }, + new SimulatorUnitEntry { + Label = "ft³/s", + Name = "ft3/s", + }, + new SimulatorUnitEntry { + Label = "cm³/s", + Name = "cm3/s", + }, + new SimulatorUnitEntry { + Label = "m³/h", + Name = "m3/h", + }, + new SimulatorUnitEntry { + Label = "m³/d", + Name = "m3/d", + }, + new SimulatorUnitEntry { + Label = "bbl/h", + Name = "bbl/h", + }, + new SimulatorUnitEntry { + Label = "bbl/d", + Name = "bbl/d", + }, + new SimulatorUnitEntry { + Label = "ft³/min", + Name = "ft3/min", + }, + new SimulatorUnitEntry { + Label = "ft³/d", + Name = "ft3/d", + }, + new SimulatorUnitEntry { + Label = "gal[UK]/h", + Name = "gal[UK]/h", + }, + new SimulatorUnitEntry { + Label = "gal[UK]/min", + Name = "gal[UK]/min", + }, + new SimulatorUnitEntry { + Label = "gal[UK]/s", + Name = "gal[UK]/s", + }, + new SimulatorUnitEntry { + Label = "gal[US]/h", + Name = "gal[US]/h", + }, + new SimulatorUnitEntry { + Label = "gal[US]/min", + Name = "gal[US]/min", + }, + new SimulatorUnitEntry { + Label = "gal[US]/s", + Name = "gal[US]/s", + }, + new SimulatorUnitEntry { + Label = "L/h", + Name = "L/h", + }, + new SimulatorUnitEntry { + Label = "L/min", + Name = "L/min", + }, + new SimulatorUnitEntry { + Label = "L/s", + Name = "L/s", + }, + } + }, + } + + + }; + } + } +} \ No newline at end of file diff --git a/Connector/config/config.example.yml b/Connector/config/config.example.yml new file mode 100644 index 0000000..ba00d9d --- /dev/null +++ b/Connector/config/config.example.yml @@ -0,0 +1,36 @@ +version: 1 + +# uncomment below to enable config from extraction pipeline +# type: remote +cognite: + project: ${CDF_PROJECT} + host: ${CDF_HOST} + idp-authentication: + tenant: ${CDF_TENANT} + client-id: ${CDF_CLIENT_ID} + secret: ${CDF_SECRET} + scopes: + - ${CDF_HOST}/.default + # uncomment below to enable extraction pipeline + # extraction-pipeline: + # pipeline-id: "pipeline name" + +connector: + name-prefix: "dwsim-connector@" + add-machine-name-suffix: true + +simulator: + name: "DWSIM" + data-set-id: ${CDF_CONNECTOR_DATASET} + +automation: + dwsim-installation-path: "C:/Users/user-name/AppData/Local/DWSIM" + +state-store: + location: "state.db" + database: LiteDb + +logger: + file: + level: "information" + path: "logs/log.txt" diff --git a/Documentation.md b/Documentation.md new file mode 100644 index 0000000..ff17d53 --- /dev/null +++ b/Documentation.md @@ -0,0 +1,47 @@ +# Cognite DWSIM connector documentation + +## Requirements +**Minimum hardware requirements** + +- 4 vCPUs (general-purpose VM) +- 16 GB RAM +- 256 GB HD for logging and storage + +The requirements for DWSIM may be higher, should accommodate for that + +**Software requirements** + +- Windows Server 2016 or higher +- DWSIM installed: The connector was built to integrate with DWSIM version 8.4. Newer versions may work, but they are not tested. +- .NET 6.0 runtime + +**Additional CDF requirements** + +- If VM is behind a firewall, allow access to endpoint: https://{cluster}.cognitedata.com. For example, if the project is in the Europe 2 (Microsoft) cluster, then this endpoint is https://westeurope-1.cognitedata.com. +- A Dataset to associate with the simulator data +- App registration for the connector, belonging to a group with the following capabilities: + - ``files:read``, ``files:write`` + - ``projects:list`` + - ``groups:list`` + - ``timeseries:read``, ``timeseries:write`` + - ``datasets:read`` + - ``extractionpipelines:read``, ``extractionpipelines:write`` + +## Installation +Running the installer on a Windows operating system will present the user with a wizard to guide her through the installation process. The user can then select the destination folder where the connector will be installed. By default, this is **C:\\Cognite\\**. Inside this folder, a working directory is created, named **DwsimConnector**. + +## Configuration +To configure the connector, the user must create a configuration file in the working directory. You can find examples of such files in the **config** folder: + + - ``config.example.yml``: A minimum configuration file that is ready to use, with only the required properties. + +The ``config.example.yml`` file can be copied to the working directory and renamed to ``config.yml``. Then, it can be opened and modified in a text editor. In this file, the configuration contained within **${}** indicates that the value is read from environment variables. The user can set these environment variables or edit the values directly. + +Once the configuration is done, the user can start the service using **Windows’ Services** application. Find the entry named **Cognite DWSIM Connector {version}** and select **Start the service**. The status will change to **Running**. The service is configured to restart automatically in case of errors or if the host machine restarts. Application log files should have been created in the configured folder. + +## Uninstalling and updating +The same MSI installer can be executed again to present the user with the option to uninstall the connector. + +Removing the installation will delete the installed binaries, remove the service and clean the application registry. The installation, however, is not deleted and still contains the configuration file, logs, and application data generated by the connector. If the connector is later re-installed, it can resume its operation using the state saved there. + +When a new version of DWSIM connector is available, the MSI installer of the new version can be executed to upgrade the connector to the new version. After the upgrade, the service will not be automatically restarted, so it needs to be manually started. \ No newline at end of file diff --git a/Installer/Installer.wixproj b/Installer/Installer.wixproj new file mode 100644 index 0000000..d6b4d3c --- /dev/null +++ b/Installer/Installer.wixproj @@ -0,0 +1,58 @@ + + + + Release + x86 + 3.10 + E9B87CE1-49FF-40B9-8F05-CAA884A662F4 + 2.0 + $(output_name)-$(target_actual_version) + Package + false + Installer + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + + + + + + + + + + + + + + + + + + + + is_pre_release=$(is_pre_release);target_product_id=$(target_product_id);target_version=$(target_version);target_actual_version=$(target_actual_version);target_description=$(target_description);product_name=$(product_name);product_short_name=$(product_short_name);build_dir=buildoutput\;exe_name=$(exe_name);service_args=$(service_args);service=$(service);config_dir=$(config_dir);upgrade_guid=$(upgrade_guid) + + + + + + \ No newline at end of file diff --git a/Installer/Product.wxs b/Installer/Product.wxs new file mode 100644 index 0000000..d1b8136 --- /dev/null +++ b/Installer/Product.wxs @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(var.Name) + Cognite Support + http://www.cognite.com + http://www.cognite.com + http://www.cognite.com + AppIcon.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Installer/Resources/InstBanner.bmp b/Installer/Resources/InstBanner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..8dfccc45c714f8185ae7810a33bcc21483d37adc GIT binary patch literal 85894 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%0DyEty*9y_x#_Dtg==JNNF9{V<}tQ{P-UX8)n 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

Hv1^l0o;AoLe{;HGOl!ui%){$& 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 aEHErEEHErEEHErEEHEtahiHM1ee8ECQps%q literal 0 HcmV?d00001 diff --git a/Installer/Resources/License.rtf b/Installer/Resources/License.rtf new file mode 100644 index 0000000000000000000000000000000000000000..7071902ccfc667664ca455c8a23d3726af81f796 GIT binary patch literal 495 zcmZ{gF;4?A42791^*>~0sFL&mRV-AnAi>fOEMC$(=gw;C97)=$s`}q?4}m~c7sru4 zKl{BSq~Z-Y&NP&|Z;idP(4hAkIBuzqo8FZp_{gQKf;1X@iW|q^`}AqS*PTlli&oE( za||v&o`f>js5e?+a)lzRB_{F%EizX$U7%H>0h<1U{;WHVM@P7Y7Wu3klUh*SVPmy2 zdZH(0rYm!T9pQdW+*GiwjT(=jcc~GNdur+uE;WMg00v9 literal 0 HcmV?d00001 diff --git a/Installer/Resources/black16x16.ico b/Installer/Resources/black16x16.ico new file mode 100644 index 0000000000000000000000000000000000000000..8686b8a9117f74d5b9e2a8acc6b98a8bd2c74974 GIT binary patch literal 1150 zcmcJOy$!-J6oemwj)I7SA{8YiBQQe7probC25FOu5g36H7=aO}Q-$xG7%7e=3=*I( zj_L@y{~jxwYfMY-46SKtkzt1~Y2wWsjTx>5=DoA%2a=+lf>^y-(r$LqGGY z@bz$v^pDn$)VFB5-sn$j!QB@l{&zmL5_De&h-MBww^-g#b9ubqKzE_h=T0)IX}vO* zCykoVt9ZY|19YYnO!kT9`V;cxSC0Ch;LgOj7oYrL9na)!>OoK1kIxIo1pjEi5r08O s;q@@j>!~CD3skB7-}H5SW$i$7F>4y;umU@q)pN8G>Rwl^rLPnCUcLo*I{*Lx literal 0 HcmV?d00001 diff --git a/Installer/Resources/black32x32.ico b/Installer/Resources/black32x32.ico new file mode 100644 index 0000000000000000000000000000000000000000..c3905a72c4cd3cfbb96860f2cb607a8808d18812 GIT binary patch literal 5430 zcmeH|ziX307{@QQYPD)(t-q?`D~fcIsvvIWKd>Mu2ue2#EGdenIU>jVG(K@vX_C7ccfqdaGe2LLo zx}7i>ubc9rI|OfG0Y*UMN6ZoCUGNm1!Cvs=51@MiuVFuE*!YIDDl4D*Jr8GK3*=w` z_QdPm%y(fLR8M=TfO2Z6-Y57Vfg#`9a0M=cYV3f1(B73>wmYC2%J&(*g3fIUe#Jay zegd!HA>07v+y`4h`7}#+8PuzCEP?vddJL*igM07@-oSHEuh(E4v=8M~KMU{F#<{L(yhY$`X#A{#g z;Uj2o8a7_}R72-<0&)rQ+Rs@y2lCbU2H!z(Zb<4{zAykc;d;DIjmc_^B4d^zUm|j7 zHM+hrT_Ew$a#;)*^9R335T|=4fCA_~st=rddCxS8Z2tD*1bD|LLEpZZ>_v^VoN?WV zS=7>eJGGHl_t$t*d|cPo=MC@fAb;7ZQTDStwt5c- zeM13?etaACQ~UGtWtFL~=ci{^-=`nHUVo0h633UAm1_bL?*c!!FB2Ce=o0rI*HXPE zyC;{C)tBC%PJ609H{$E>c&q+Y2XqGNZ5I6g)RS^e!kYd1`CG|;e&64>Bde#1qp!s} z^{c&R`o^3*^?kOgZ|%edpmXxiN3!};pXy0IfsONLY)$bSwUe(1C0O)x$GWoYwvOkw zX!UmXb6CBdv)b`j{igqMWIeZn&FRkQyLvuu?snFj^*4O}dKc|8oNedKT^}db<_Oa4 zuFbB=F%AVQXPJp}zoX3GN0wSizSPfyzG>YhYwk95_FU||#wRInw{-tO{TX_w#T2`> zugyHYJ%Pv_Ap%q(3`3WOu!It=iT;LWdlb%@OZ?=d848hznX7t%(R^6Nfv|a!3)ar~ E2Nv*A-T(jq literal 0 HcmV?d00001 diff --git a/Installer/build.ps1 b/Installer/build.ps1 new file mode 100644 index 0000000..70bcf7b --- /dev/null +++ b/Installer/build.ps1 @@ -0,0 +1,41 @@ +Param( + [Parameter(Mandatory=$true)] + [Alias("b")] + [string]$msbuild, + + [Parameter(Mandatory=$true)] + [Alias("v")] + [string]$version, + + [Parameter(Mandatory=$true)] + [Alias("d")] + [string]$description, + + [Parameter(Mandatory=$true)] + [Alias("c")] + [string]$config +) + +$Args = "" +$Settings = Get-Content -Path $config | ConvertFrom-Json +foreach ($prop in $Settings.PsObject.Properties) { + if ($prop.Name -eq "setup_project") { + continue + } + $Args = "$($Args) /p:$($prop.Name)=`"$($prop.Value)`"" +} + +$ValidVersion = $version +$IsPreRelease = "no" +if ("$($version)" -match '-alpha.' -or "$($version)" -match '-beta.') +{ + $IsPreRelease = "yes" + Write-Host "Creating a pre-release installer: $($version)" + + $ValidVersion = $version -replace '-alpha.*','' + $ValidVersion = $ValidVersion -replace '-beta.*','' +} + +$Args = "$($Args) /p:is_pre_release=`"$($IsPreRelease)`" /p:target_version=`"$($ValidVersion)`" /p:target_actual_version=`"$($version)`" /p:target_description=`"$($description)`"" + +Invoke-Expression "$msbuild $($Settings.setup_project) $Args" \ No newline at end of file diff --git a/Installer/setup-config.json b/Installer/setup-config.json new file mode 100644 index 0000000..c4ce67e --- /dev/null +++ b/Installer/setup-config.json @@ -0,0 +1,13 @@ +{ + "target_project": "../Service/Service.csproj", + "target_configuration": "Release", + "target_runtime": "win-x64", + "product_name": "Cognite DWSIM Connector", + "product_short_name": "DwsimConnector", + "exe_name": "Service.exe", + "config_dir": "../Connector/config", + "service": "true", + "service_args": "-s", + "upgrade_guid": "3AE47272-FAD2-499E-A5FF-22F4368B2EF5", + "output_name": "DwsimConnectorInstaller" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a9a151 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# DWSIM Cognite Data Fusion Connector .NET + +DWSIMConnector is a connector that integrates DWSIM simulator with CDF (Cognite Data Fusion). Once installed at a host machine with access to DWSIM, the connector reads model files and routines defined in CDF simulators API and uses them to run DWSIM simulations on schedule. The simulation results are saved in CDF as simulation data or, optionally, as time series data points. + +This connector uses COM (Component Object Model) to communicate with DWSIM. It extends the [simulator utils](https://github.com/cognitedata/dotnet-simulator-utils) by implementing specific logic for the COM interface, serving as a practical example for creating custom CDF connectors to other COM-compatible simulators. + + +# Running the connector + +- Clone this repository +- Run `dotnet build` +- Copy the `config/config.example.yml` into the current working directory and rename it to `config.yml` +- Modify the `config.yml` file with your own Cognite Data Fusion credentials +- Run `dotnet run Service/Service.csproj` + + +# Building an installer + +This repo comes with a setup creation template for the DWSIM connector. Here's how to use it: + +- Change directory to the `Installer` folder +- Run `\build.ps1 -b msbuild -v ${{ VERSION_OF_DWSIM_CONNECTOR_HERE }} -d "DWSIM connector Installer" -c .\setup-config.json` + +# About the code + +Each simulator connector is comprised of the following 4 parts: + +## The Connector Runtime + +The connector runtime is a wrapper that runs the entire connector. It connects to CDF (Cognite Data Fusion), downloads model files, runs simulations, and handles the scheduling of simulations. To implement this in your own connector, you need to register your classes in the ConfigureServices function, define a ConnectorName, and you should be all set. An example for this can be seen in this repo [here](Connector/ConnectorRuntime.cs). + +## The Connector configuration + +The connector accepts configuration via a [config.yml](Connector/config/config.example.yml) file. This file contains CDF (Cognite Data Fusion) credentials, local state storage configuration and simulator specific configuration. Simulator specific configuration is nested under the `automation` tag. If you want to define any other variables to be read from that file, you must define them in the [AutomationConfig.cs](Connector/Dwsim/AutomationConfig.cs) file first. + +## The simulator client + +The simulator client is the part which actually connects to the simulator itself and acts as a bridge between CDF and the simulator. It has methods which are used to get the Simulator version, connector version, run a simulation or extract simulator model information. + +[Source](Connector/Dwsim/DwsimClient.cs) + +## The routine + +This is used by the simulation runner to run a routine, it implements methods to set data into a simulation and get data from a simulation. + +[Source](Connector/Dwsim/DwsimRoutine.cs) + + +## License + +Apache v2, see [LICENSE](./LICENSE). + +## Contribution + +To maintain the consistency and quality of the repo, please ensure any contributions adhere to the established structure and guidelines. Before submitting any additions or modifications, ensure that the code builds successfully. \ No newline at end of file diff --git a/Service/Program.cs b/Service/Program.cs new file mode 100644 index 0000000..669c29e --- /dev/null +++ b/Service/Program.cs @@ -0,0 +1,86 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.NamingConventionBinder; +using System.CommandLine.Parsing; + +namespace Service; + +public class Program +{ + public static int Main(string[] args) + { + return GetCommandLineOptions().InvokeAsync(args).Result; + } + private static Parser GetCommandLineOptions() + { + var rootCommand = new RootCommand(); + + var flag = new Option("--service", "Required flag when starting the connector as a service"); + flag.AddAlias("-s"); + rootCommand.AddOption(flag); + + var dir = new Option("--workdir", "Indicates the working directory to run the connector from"); + dir.AddAlias("-w"); + rootCommand.AddOption(dir); + + rootCommand.Handler = CommandHandler.Create((ConnectorParams setup) => + { + if (setup.Service) + { + RunService(setup); + } + else + { + RunStandalone(); + } + }); + + return new CommandLineBuilder(rootCommand) + .UseVersionOption() + .UseHelp() + .Build(); + } + + private static void RunStandalone() + { + Connector.ConnectorRuntime.RunStandalone().Wait(); + } + + private static void RunService(ConnectorParams setup) + { + var builder = Host.CreateDefaultBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddSingleton(setup); + services.AddHostedService(); + }); + builder = builder.ConfigureLogging(loggerFactory => loggerFactory.AddEventLog()) + .UseWindowsService(options => options.ServiceName = "DwsimConnector"); + builder.Build().Run(); + } +} + +public class ConnectorParams +{ + public bool Service { get; set; } + public string WorkDir { get; set; } +} diff --git a/Service/Service.csproj b/Service/Service.csproj new file mode 100644 index 0000000..a624ec1 --- /dev/null +++ b/Service/Service.csproj @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + Exe + net6.0 + enable + enable + false + + + + + diff --git a/Service/Worker.cs b/Service/Worker.cs new file mode 100644 index 0000000..dca6cb1 --- /dev/null +++ b/Service/Worker.cs @@ -0,0 +1,71 @@ +/** + * Copyright 2024 Cognite AS + * + * 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. + */ + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Service; + +public class Worker : BackgroundService +{ + private readonly ILogger _eventLog; + private readonly ConnectorParams _setup; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + + public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger eventLog, ConnectorParams setup) + { + _hostApplicationLifetime = hostApplicationLifetime; + _eventLog = eventLog; + _setup = setup; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _eventLog.LogInformation("Starting DWSIM connector service"); + + try + { + // Move to the connector working directory + var path = _setup.WorkDir; + if (path == null) + { + path = Directory.GetParent(AppContext.BaseDirectory)?.Parent?.FullName; + } + if (string.IsNullOrEmpty(path)) + { + _eventLog.LogError("Invalid working directory"); + return; + } + if (!Directory.Exists(path)) + { + _eventLog.LogError("Invalid working directory: {Message}", path); + return; + } + Directory.SetCurrentDirectory(path); + + // Start the connector loop + await Connector.ConnectorRuntime + .Run(_eventLog, stoppingToken) + .ConfigureAwait(false); + } + finally + { + // On exit, finalize the host application as well + _eventLog.LogWarning("Exiting DWSIM connector service"); + _hostApplicationLifetime.StopApplication(); + } + } +} diff --git a/dwsim-connector-dotnet.sln b/dwsim-connector-dotnet.sln new file mode 100644 index 0000000..9cf74cb --- /dev/null +++ b/dwsim-connector-dotnet.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{F82A981C-4841-43E5-A90F-DD567DFE43E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connector", "Connector\Connector.csproj", "{03FE7CB5-D98A-4FE4-A364-381CA2DB32BA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F82A981C-4841-43E5-A90F-DD567DFE43E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F82A981C-4841-43E5-A90F-DD567DFE43E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F82A981C-4841-43E5-A90F-DD567DFE43E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F82A981C-4841-43E5-A90F-DD567DFE43E9}.Release|Any CPU.Build.0 = Release|Any CPU + {03FE7CB5-D98A-4FE4-A364-381CA2DB32BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03FE7CB5-D98A-4FE4-A364-381CA2DB32BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03FE7CB5-D98A-4FE4-A364-381CA2DB32BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03FE7CB5-D98A-4FE4-A364-381CA2DB32BA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9a8527e584613f42ed3868d1d778c4d6e7b70a47 GIT binary patch literal 3923 zcmY*cXIN8Nv<(E1&_*diI!KWgAW^Cz2nK1P2vG<<^w3KfszQJWN>c(sI?|gU3Q|-g zAfO;pq=+Id6cOnlZNwL5d^7Kz`|Z2e*=Oyw&pzM%anBty6Fqhoeii@#z;2+AG^cs# z6Jutgy_1fceQ1s@$XrhcQ2SnRnKp@YwK8xsHU`MjFf)LWjuQYpq0k-x9Y28aZwvq! z(h2;F&FQ3nbLatpcpQM?H^-XhPlhASXxYC#u#oOQ#X|aj+0uo;|1gaWa3d^v&>YBL z9}@%saJ)V-x(qoE0h$^Hhql678KX3u{d^R#E`Cm~3ZXv!Cs_b^s0I!CxZ<&*p+4Te zK^mc&=l*7B(C|rE@to-26ug(_IV)o`(JOv|uA(XmN(xHn5GKGi7mU|DckZOnzvG{M;&E>ORq_q`-4?Au#gi39sDhH>zuL4^_(@d5 zG!W-Xt9+u5fWrS~{=eAoI&j64;{Rte|8)9yl-4SO1+Mt-u_0La>}OBWJ{OsX=*T=D0>WZ2L#1)a2yM`ko6vyz8bzrr;6&cfw~6 z+CtV|E=9VxEahJ!IMrJL%sP7(faAfdAg0*EhP}(8j_a(y_IJ!%l0=tAMn?RLRu;Dz z8R)fdk>dBSgYP!Ix+-z=$awuaOu4<1U|wh@&3zyitt!942XV31tdpnvYaOZM6rUA& zUxz*+&eKemY$oBaOsMl6eNnv}K5mt@A{%5l&(<5zAM<@l4fJtIS)?JF!DAUO72kPY zfr_4%&NVxIX}oDJg}skGv;}=z-nM`3*JhcmNx&Vb8OQvDKJ!uHUu{KnjhoTEu%ZL1 zn~F(V-yF{{-+ENxaey5;(PIz;tPAA6bAGwc@kd#wxV()Yra?fOMXjB;xV z3~m!@;MvZDZScsvQ)S#~YK4?lY-If#ga=A@MZ|>q!KiagYwMG`x36itn5K^IRDnca zQR338U;(EgrM4&az+F&0`NfbNqCF>i9GM0@xc9KiV+ryY`ZTwTGwVG!OM-3(gS;hl zutkH^mqTyy{Z4PYNY>ab;Is4R)HKxCi6OOCi-+gF%}zQ(30pa#;ZHPKrWs8{Y61L> z;pP*NPEfp3_WQX3zXHl^J`}#W`nkTbI&VrY=d&j+Ydm%Lp}wj(pZ2=-F{Q)v?!J~H@EH!@30xefEI!;mHYRHInpb&mW4ZR=A2DTdf8-RPbo{$ZGZkU zK^aL&I`097f7>RebyZIpi$;J%a^4k1xo~1qs9pvo-A}%@1qvAl2OyUGGHUU*)GX zhT^$?QbX4#MITGYfTR{rGu}-Mn46SW&4^Z?FYeF=k4nv-UbCG}aj5ar`4-6^(e#Wu zW|RF+spGOOx(XRa;a_zc@~#iG23|N8lH*^MNHG72e%tjk=W~4(!t?pH+T+f*+G}eV ziI#xwC#d%&*UE_f7kTn#BTkX_+IUlEOGC6eOGlc!oRMNz3}D?|)y4Y~U1|=(J@tLX z%^eTY>111=r&)++T%TSPzjy2`b6NHYH2EB59P67a_yux3?Hw+D?@Qvpq!KIA76@*Fw&h12NFUafUK0q}m{`Di-#XNcR7&f5U#519$DcO+ zxx#5$j)}|So4XTdD>oT%q>W?I+1hTmf#C!wLc-?3?IM)f0 zVPbxnP1n3?Vra%uFNiJYn|SC}9s<5Isdu&+@is_3=IApgSrUel)4yH+Yzpg}EXJSM z*N6`x2+NVHIF%XAV%7rr_#|!~iA)lY_?^`Zo08rEG=1e{%Q_# z*o~1nOI7OxV``5Ucr&T(sn49IZ%YZbk##MN1WkoRIx1OXt!#Natrtz$28ya13Ala= zUZqE;>1(VroZKtc6ELS^%Qa`9{lPF|`Ytxc&X97-^pC;<$#hg5i+g94TeX{pUir|F zZBKMx@v}!{UkC+TDYCc;)gO!q33j@o$903N{myLa=CNASA43Iee3VZql1TxE&NC(h z+@Wm|m+WyyOk5mK9i_Qhki{GyBY@m9$gd_@gyabzWReo_nQ#?W7~n$=x)OTquQ~|s zQL?MED=3?w{;5o7f3Q)M_q0j=QBP_Jeo$yq2i?4Mc#&QidvW6pX%}*9S%~g}AB^Wf zfdhIPqn**&Vw=)c8jJMN$rRbn+&VbRIMr4Jx><~QTH~~_O4YpaOY^g)XTW3lnQl?# zfQn~hY-TOSY=8AQyh?CQ`fzI9CN+2!Wp@|2Wn?_Bq^#LlJZe*DEOYru{x}zY!bCx} z!iZ!mxU^;@I{nY4*BY+8=6WTFUx8QzHM6;o?7r+^b z$4VzxzOZC|9X6bMbdWiCwW$DWt3U-ouUzJdBzrOg8HY=flgj386nKW-3YLQmA?jo4 zFydl`+MwWcm;DX0LT&X$=d=&Y4VNxeUufs;+Arq2+OjV^HT)^9)grXi%m=k_sXWjY z{K)q(eHW#6-T>1-H0n-X+0`bKP2{JC@3wjELY%XJ6Ma>4;@8|3H#b4^AedFaQ+AJg z^smvGWo!fUny)UsZi_aNn(2Gd)umh;#%e<(T$Y7@Cj~JUwNJA$pe`Y&g_Y`>LQ*pY zVCi=u#~;Kr5m3So`l>RxyxBu4R@Kz5Yr%l2F=Mg4c&}#urdsrx_Zi3zHYH7FFDi$y zY3uFiecRIPK_deuNWTDx*)_>-v3g0^Msr^Y&$E{6LJi?V>a1f_giU1|(d<#{Q#PBU zKGhH&1NJ-$XHl8G<7y7v5Z(HE-(oOvP4#VVf&9T?Zc|}0$z|Lm`H<^7G3jQ%hmkLW zhhCw(yE|*uT?rm3M8(*rdg!){omyahg<0n#h>uG96uDpfLokdF9I(^QfB9$xz#EZm zAXKM;L4Mk_2eb0!b2T7+pGC7)LKiI`aSA9RR;g}LNg3|ZEwk$U4h&W(*><)<0@pGV zZ$!dNP*6>?y%x(Rpgf|{AkJ&g0rNOtsQa!JJCi{sg;(Y6&ga}g7DC#))5n5Gix2v% z>1ESa=hT^OAL<^G(F?HBtUNoTAxcS2peBWLWqtbl>#A<)@UTYPjjLbhq~R3uJ=C(UKBlO)lflb89$t_J7z*e37&kcO~oYmFuA-dIG1 zEWjtP z&aYS%%TY)-{W7qzH36C7A}e3?LU?0T%JWfeCYDDOPvUZ8d>wLqJ74l9!(>-)EJ6tc zO@^&SbX zBQ#M3A{J#8o&;rH6!tP^yalKmr)|q^#K$ydBjumL*EhY z2ZHg|p^2h()&o^l4K*c|#Ky=>W!aQ^^(m3mvD1IdavIgV^s{exT1O)4UcfQZ-`t^c zgD())aLLqCQn3{!&7n!FWhzyVe*nNwh>{2QmI72B*~Xn38^iuu4ye}Oi7g3frkAmX z>6&egGd`EIP{x%@9oll;pPpZ7@~wSTmQ0RO3(dJM)m zRgounsyipq>Ft&+in5)(Vy87DUzs}l(60Kx#K98!gAu)V9FAL6wYVy>EfMu*MXvYT zxV2_0$t#S9+r|f6<0fAv1u6;uVgc^Gr=ePobHQ31Z+~SS|A;C|8^7pGrzUW$czn@e UPFL%J{mISJK-UCWtK%5+KNsEZ)&Kwi literal 0 HcmV?d00001 diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 0000000..3c10750 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,120 @@ +artifacts: + - name: DwsimConnectorInstaller-{version}.msi + path: DwsimConnectorInstaller-{version}.msi + platform: windows + - name: DwsimConnector-{version}.pdf + path: Documentation.pdf + platform: docs + displayName: Connector Documentation + +extractor: + externalId: dwsim + name: Cognite DWSIM Connector + description: Integrates the DWSIM simulator with CDF + documentation: > + The Cognite DWSIM Connector integrates the DWSIM simulator with CDF. The connector should be configured and installed in the same host machine running the simulator. + It reads model files and simulation configurations from CDF, invokes DWSIM to run the simulations, and saves the results back to CDF as time series. + tags: ["simulator", "dwsim", "opensource"] + type: "unreleased" + links: + - name: Documentation (WIP) + url: "https://pr-1064.docs.preview.cogniteapp.com/cdf/integration/concepts/simulator/" + type: "externalDocumentation" + image: logo.png + +versions: + "1.0.0-alpha-117": + description: Optional data sampling + changelog: + added: + - "Added support for optional data sampling" + - "Historical order of simulation runs" + "1.0.0-alpha-116": + description: Add simulator definition + changelog: + added: + - "Added simulator definition (containing units, simulator info, model file extension) etc to initialize the simulator on the Cognite Data Fusion API." + "1.0.0-alpha-115": + description: Attempt to fix automation slowness + changelog: + fixed: + - "Fix memory leak due to a pile-up of DWSIM flowsheet objects" + "1.0.0-alpha-114": + description: Add array values support + changelog: + added: + - "Support for getting & setting material stream composition" + - "Support for getting material stream components" + "1.0.0-alpha-113": + description: Support time series inputs overrides + "1.0.0-alpha-112": + description: Publish binary + "1.0.0-alpha-111": + description: Release binary + "1.0.0-alpha-110": + description: Fix connector version in binary + "1.0.0-alpha-109": + description: Update dotnet-simulator-utils, using default connector runtime from utils now + "1.0.0-alpha-108": + description: Support model revision re-parsing + "1.0.0-alpha-107": + description: Adopt the latest version of simulator utils + "1.0.0-alpha-106": + description: Support breaking changes in the simulator integration API + changelog: + fixed: + - "Breaking changes in the simulator routine units (quantity)" + "1.0.0-alpha-105": + description: Model revision parameter per simulation run + changelog: + added: + - "Added support for custom model revision per simulation run" + fixed: + - "Fixed model parsing logic and logs" + "1.0.0-alpha-104": + description: Minor improvements and bug fixes + changelog: + fixed: + - "Minor improvements and bug fixes" + added: + - "Support model parsing logs" + "1.0.0-alpha-103": + description: Extended inputs and outputs, overrides + changelog: + added: + - "Added support for extended inputs and outputs, as well as input overrides" + "1.0.0-alpha-102": + description: Minor improvements + changelog: + added: + - "Added support for the remote API logs for the simulation runs" + "1.0.0-alpha-101": + description: Minor improvements + changelog: + added: + - "Added support for the simulationTime property on simulation run object" + "1.0.0-alpha-100": + description: Powered by the new simulator integration API + changelog: + added: + - "Added support for the new simulator integation API (breaking change)" + "1.0.0-alpha-007": + description: Cognite DWSIM connector + changelog: + added: + - "Added support for remote config" + "1.0.0-alpha-006": + description: Cognite DWSIM connector + changelog: + added: + - "Upload documentation pdf to CDF" + "1.0.0-alpha-005": + description: Cognite DWSIM connector + changelog: + added: + - "Signed binary" + "1.0.0-alpha-001": + description: Cognite DWSIM connector + changelog: + added: + - "First alpha version"