diff --git a/.github/actions/create-build-tag/action.yml b/.github/actions/create-build-tag/action.yml new file mode 100644 index 0000000..2db5b11 --- /dev/null +++ b/.github/actions/create-build-tag/action.yml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +name: create-build-tag + +outputs: + build_tag: + description: 'The generated build tag' + value: ${{ steps.create-build-tag.outputs.build_tag }} +runs: + using: "composite" + steps: + - name: Create build tag + id: create-build-tag + env: + # head_ref is available when a pull request triggers workflow, ref_name contains branch name outside of pull requests + BRANCH_NAME_RAW: ${{ github.head_ref || github.ref_name }} + COMMIT_SHA_RAW: ${{ github.sha }} + run: | + DATETIME=`date -u +"%Y-%m-%dT%H-%M-%S"` + BRANCH_NAME=`echo "${BRANCH_NAME_RAW}" | cut -c1-20 | sed 's|/|-|g'` + COMMIT_SHA=`echo "${COMMIT_SHA_RAW}" | cut -c 1-7` + echo "build_tag=${BRANCH_NAME}-${DATETIME}-${COMMIT_SHA}" >> "$GITHUB_OUTPUT" + shell: bash \ No newline at end of file diff --git a/.github/actions/setup-runtime/action.yml b/.github/actions/setup-runtime/action.yml new file mode 100644 index 0000000..9581c94 --- /dev/null +++ b/.github/actions/setup-runtime/action.yml @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +name: Java & Gradle Setup +description: Setup runtime + +inputs: + java_version: + description: 'The Java version to install.' + required: false + default: '25' + gradle_version: + description: 'The Gradle version to install. It do not force using this gradle version in other workflow steps, normally the installed gradle version of the project will be used.' + required: false + default: '9.1.0' + fetch-depth: + description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.' + default: '1' + enable_caching: + description: 'If build and configuration caching should be enabled or disabled.' + default: 'true' + enable_daemon: + description: 'If build should be done using a separate gradle daemon.' + default: 'false' + +runs: + using: "composite" + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 + with: + fetch-depth: ${{ inputs.fetch-depth }} + + - name: Set up JDK + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # 5.6.0 + with: + java-version: ${{ inputs.java_version }} + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # 6.2.0 + with: + gradle-version: ${{ inputs.gradle_version }} + + - name: Cache Gradle + if: ${{ inputs.enable_caching == 'true' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/settings.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Configure Gradle properties + shell: bash + env: + CACHING: ${{ inputs.enable_caching }} + DAEMON: ${{ inputs.enable_daemon }} + run: | + set -euo pipefail + mkdir -p ~/.gradle + { + echo "ci=true" + echo "org.gradle.caching=${CACHING}" + echo "org.gradle.configuration-cache=${CACHING}" + echo "org.gradle.daemon=${DAEMON}" + } >> ~/.gradle/gradle.properties \ No newline at end of file diff --git a/.github/workflows/java-gradle-ci.yml b/.github/workflows/java-gradle-ci.yml new file mode 100644 index 0000000..88ce916 --- /dev/null +++ b/.github/workflows/java-gradle-ci.yml @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +name: Java CI with Gradle for library + +on: + push: + workflow_dispatch: + inputs: + local_image_tag: + description: Local image tag used before push + required: false + type: string + default: application:0.0.1-SNAPSHOT + deploy_branch: + description: Branch required to push image + required: false + type: string + default: main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + permissions: + id-token: write # Needed to load credentials + contents: read # Needed to read repository (content) + outputs: + build_tag: ${{ steps.create_tag.outputs.build_tag }} + environment: + name: ci + env: + LOCAL_IMAGE_TAG: ${{ inputs.local_image_tag }} + DEPLOY_BRANCH: ${{ inputs.deploy_branch }} + IMAGE_REPOSITORY_PREFIX: ${{ inputs.image_repository_prefix }}${{ inputs.namespace }} + IS_TAG: ${{ github.ref_type == 'tag' }} + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Setup runtime + uses: ./.github/actions/setup-runtime + + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/settings.gradle*') }} + + - name: Optimize Gradle + shell: bash + run: | + mkdir -p ~/.gradle + echo "org.gradle.caching=true" >> ~/.gradle/gradle.properties + echo "org.gradle.configuration-cache=true" >> ~/.gradle/gradle.properties + + - name: Build & Run Tests + run: ./gradlew build --configuration-cache + + - name: Create Build Tag + id: create_tag + uses: ./.github/actions/create-build-tag + + - name: Repository Name + id: repo + shell: bash + run: | + echo "name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT" diff --git a/README.md b/README.md index 4bb4b22..f2aa510 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,33 @@ SPDX-License-Identifier: MPL-2.0 [![Power Grid Model logo](https://raw.githubusercontent.com/PowerGridModel/.github/main/artwork/svg/color.svg)](#) -# Power Grid Model template repository +# libpower-grid-model-java + +## Usage + +The PGM methods are available through class `org.lfenergy.pgm.PowerGridModelC`. The method names are identical to the method names in the C-documentation. + +To load the native library there are two options: +- on supported architectures, the library can be autoloaded using `PGMLoader.autoload()` +- on other architecture, you can build and load the library yourself using `System.load(...)`, and verify if you've loaded it correctly using `PGMLoader.check()`. + +## Supported architectures + +- macOS ARM64 +- macOS x86-64 +- Linux ARM64 +- Linux x86-64 +- Windows x86-64 -This is a template repository for the Power Grid Model project. -Only common and mandatory content is included in this repository. -The derived repositories should review and edit content relevant to the best practices and standards. ## Checklist after creating a repository from this template Update the following items manually before publishing your repository: -- [ ] Rename the repository and update all references in this file. -- [ ] Replace all `pgm-template-repo` references across the repository, especially in this README and REUSE.toml. -- [ ] Replace the project title and description in this README. -- [ ] Ensure all third-party licenses are present in [LICENSES](https://github.com/PowerGridModel/pgm-template-repo/tree/main/LICENSES). +- [x] Rename the repository and update all references in this file. +- [x] Replace all `pgm-template-repo` references across the repository, especially in this README and REUSE.toml. +- [x] Replace the project title and description in this README. +- [ ] Ensure all third-party licenses are present in [LICENSES](https://github.com/PowerGridModel/libpower-grid-model-java/tree/main/LICENSES). ### Optional checks @@ -28,14 +41,14 @@ Change these if applicable. - [ ] Replace Power Grid Model project-specific links (for example contributing, support, release, security, and code of conduct links) with links for your project. - [ ] Add logos and badges corresponding to relevant pages -- [ ] Verify [LICENSE](https://github.com/PowerGridModel/pgm-template-repo/blob/main/LICENSE) copyright holder and year(s). +- [ ] Verify [LICENSE](https://github.com/PowerGridModel/libpower-grid-model-java/blob/main/LICENSE) copyright holder and year(s). Also verify SPDX copyright headers in source and documentation files. - [ ] Include documents present in [Home page of PGM org](https://github.com/PowerGridModel/.github/) in documentation if they are created. ## License This project is licensed under the Mozilla Public License, version 2.0 - see -[LICENSE](https://github.com/PowerGridModel/pgm-template-repo/blob/main/LICENSE) for details. +[LICENSE](https://github.com/PowerGridModel/libpower-grid-model-java/blob/main/LICENSE) for details. ## Licenses third-party libraries @@ -43,7 +56,7 @@ This project includes third-party libraries, which are licensed under their own respective Open-Source licenses. SPDX-License-Identifier headers are used to show which license is applicable. The concerning license files can be found in the -[LICENSES](https://github.com/PowerGridModel/pgm-template-repo/tree/main/LICENSES) directory. +[LICENSES](https://github.com/PowerGridModel/libpower-grid-model-java/tree/main/LICENSES) directory. ## Contributing diff --git a/REUSE.toml b/REUSE.toml index 1f65083..b08bc14 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -3,9 +3,9 @@ # SPDX-License-Identifier: MPL-2.0 version = 1 -SPDX-PackageName = "pgm-template-repo" +SPDX-PackageName = "libpower-grid-model-java" SPDX-PackageSupplier = "Power Grid Model project " -SPDX-PackageDownloadLocation = "https://github.com/PowerGridModel/pgm-template-repo" +SPDX-PackageDownloadLocation = "https://github.com/PowerGridModel/libpower-grid-model-java" [[annotations]] path = "./**" diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0de00b2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,344 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +import javax.inject.Inject + +plugins { + id 'java' + id 'maven-publish' + id("com.diffplug.spotless") version "8.9.0" +} + +interface InjectedExecOps { + @Inject + ExecOperations getExecOperations() +} + +group = 'org-power-grid-model' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +apply plugin: 'checkstyle' +apply plugin: 'pmd' + +checkstyle { + toolVersion = "13.5.0" + sourceSets = [project.sourceSets.main] +} + +checkstyleMain { + configFile = "${rootDir}/config/checkstyle/checkstyle_main.xml" as File +} + +checkstyleTest { + configFile = "${rootDir}/config/checkstyle/checkstyle_test.xml" as File +} + +pmd { + consoleOutput = true + toolVersion = '7.26.0' + ruleSetFiles = files("${rootDir}/config/pmd/ruleset.xml") +} + +pmdTest { + enabled = false +} + +tasks.named("pmdMain", Pmd) { + source = fileTree("src/main/java") +} + +spotless{ + java { + targetExclude("build/**") + toggleOffOn() + eclipse().configFile('config/spotless/code_style.xml') + + licenseHeader """// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +""" + } +} + +def pgmVersion = "1.13.109" + +def pgmDir = layout.buildDirectory.dir("pgm").get() +def wheelDir = pgmDir.dir("download") +def headersDir = pgmDir.dir("headers") +def nativeLibDir = pgmDir.dir("lib") +def generatedJavaDir = pgmDir.dir("generated-src") + +def architectures = [ + "x86_64_macosx": [ + url: "https://github.com/PowerGridModel/power-grid-model/releases/download/v$pgmVersion/power_grid_model-$pgmVersion-py3-none-macosx_13_0_x86_64.whl", + wheelFile: wheelDir.file("power_grid_model_x86_64_macosx.whl"), + nativeLibPathInWheel: "power_grid_model/_core/power_grid_model_c/lib/libpower_grid_model_c.dylib", + nativeLibFile: nativeLibDir.file("power_grid_model_c_x86_64_macosx.dylib") + ], + "arm64_macosx": [ + url: "https://github.com/PowerGridModel/power-grid-model/releases/download/v$pgmVersion/power_grid_model-$pgmVersion-py3-none-macosx_13_0_arm64.whl", + wheelFile: wheelDir.file("power_grid_model_arm64_macosx.whl"), + nativeLibPathInWheel: "power_grid_model/_core/power_grid_model_c/lib/libpower_grid_model_c.dylib", + nativeLibFile: nativeLibDir.file("power_grid_model_c_arm64_macosx.dylib") + ], + "x86_64_windows": [ + url: "https://github.com/PowerGridModel/power-grid-model/releases/download/v$pgmVersion/power_grid_model-$pgmVersion-py3-none-win_amd64.whl", + wheelFile: wheelDir.file("power_grid_model_x86_64_windows.whl"), + nativeLibPathInWheel: "power_grid_model/_core/power_grid_model_c/bin/power_grid_model_c.dll", + nativeLibFile: nativeLibDir.file("power_grid_model_c_x86_64_windows.dll") + ], + "arm64_linux": [ + url: "https://github.com/PowerGridModel/power-grid-model/releases/download/v$pgmVersion/power_grid_model-$pgmVersion-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", + wheelFile: wheelDir.file("power_grid_model_arm64_linux.whl"), + nativeLibPathInWheel: "power_grid_model/_core/power_grid_model_c/lib64/libpower_grid_model_c.so", + nativeLibFile: nativeLibDir.file("power_grid_model_c_arm64_linux.so") + ], + "x86_64_linux": [ + url: "https://github.com/PowerGridModel/power-grid-model/releases/download/v$pgmVersion/power_grid_model-$pgmVersion-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", + wheelFile: wheelDir.file("power_grid_model_x86_64_linux.whl"), + nativeLibPathInWheel: "power_grid_model/_core/power_grid_model_c/lib64/libpower_grid_model_c.so", + nativeLibFile: nativeLibDir.file("power_grid_model_c_x86_64_linux.so") + ] +] + +tasks.register("extractPGMNativeLibsFromWheels") { + doLast { + nativeLibDir.file("version").asFile.write(pgmVersion) + } +} + +architectures.forEach { slug, architecture -> + def downloadWheelTaskName = "downloadPGMWheel_$slug" + def extractNativeLibTaskName = "extractPGMNativeLibFromWheel_$slug" + + tasks.named("extractPGMNativeLibsFromWheels") { + dependsOn(tasks.named(extractNativeLibTaskName)) + } + + tasks.register(downloadWheelTaskName) { + GString url = architecture.url + RegularFile wheelFile = architecture.wheelFile + + outputs.file(wheelFile) + + doLast { + def target = wheelFile.asFile + target.parentFile.mkdirs() + + URI.create(url).toURL().withInputStream { input -> + target.withOutputStream { output -> + output << input + } + } + } + } + + tasks.register(extractNativeLibTaskName, Copy) { + dependsOn(tasks.named(downloadWheelTaskName)) + + RegularFile wheelFile = architecture.wheelFile + RegularFile nativeLibFile = architecture.nativeLibFile + String nativeLibFilename = nativeLibFile.asFile.name + String nativeLibPathInWheel = architecture.nativeLibPathInWheel + + // Ensure the target directory exists before extracting the library + doFirst { + nativeLibDir.asFile.mkdirs() + } + + // Register the file as an output of the task to ensure Gradle tracks it correctly + outputs.file(nativeLibFile) + + from(zipTree(wheelFile)) { + include nativeLibPathInWheel + + eachFile { FileCopyDetails details -> + if (details.path == null) { + return + } + println(details.path) + details.path = nativeLibFilename + } + } + + includeEmptyDirs false + into(nativeLibDir) + } +} + +tasks.register("extractPGMHeaderFiles", Copy) { + dependsOn(tasks.named("extractPGMNativeLibFromWheel_x86_64_linux")) + + RegularFile wheelFile = architectures.x86_64_linux.wheelFile + + doFirst { + headersDir.asFile.mkdirs() + } + + def internalBaseDir = "power_grid_model/_core/power_grid_model_c/include" + from(zipTree(wheelFile)) { + include "$internalBaseDir/**/*.h" + + eachFile { FileCopyDetails details -> + if (details.path == null) { + return + } + + details.path = details.path.replace("$internalBaseDir/", "") + } + } + includeEmptyDirs false + into headersDir +} + +def jextractDir = layout.buildDirectory.dir("jextract").get() +def jextractArchive = jextractDir.file("jextract.tar.gz") + +def osName = System.getProperty("os.name").toLowerCase() +def osArch = System.getProperty("os.arch").toLowerCase() + +def jextractUrlPropertyKey = { + if (osName.contains("linux")) { + return osArch in ["aarch64", "arm64"] + ? "jextract_linux_aarch64_url" + : "jextract_linux_x64_url" + } + + if (osName.contains("mac")) { + return osArch in ["aarch64", "arm64"] + ? "jextract_macos_aarch64_url" + : "jextract_macos_x64_url" + } + + if (osName.contains("windows")) { + if (!(osArch in ["amd64", "x86_64"])) { + throw new GradleException( + "Unsupported Windows architecture: $osArch" + ) + } + + return "jextract_windows_x64_url" + } + + throw new GradleException( + "Unsupported platform: $osName / $osArch" + ) +}() + +def jextractUrl = project.property(jextractUrlPropertyKey) + +tasks.register("downloadJextract") { + outputs.file(jextractArchive) + doLast { + def target = jextractArchive.asFile + target.parentFile.mkdirs() + URI.create(jextractUrl).toURL().withInputStream { input -> + target.withOutputStream { output -> + output << input + } + } + } +} + +tasks.register("extractJextract", Copy) { + dependsOn(tasks.named("downloadJextract")) + from tarTree(resources.gzip(jextractArchive.asFile)) + into jextractDir +} + +tasks.register("generatePGMBindings") { + // Use a local jextract installation by setting jextract_bin in gradle.properties, + // otherwise jextract is downloaded and extracted automatically. + def localJextractBin = project.findProperty("jextract_bin") + if (!localJextractBin) { + dependsOn(tasks.named('extractJextract')) + } + dependsOn(tasks.named('extractPGMNativeLibsFromWheels')) + dependsOn(tasks.named('extractPGMHeaderFiles')) + def execOps = project.objects.newInstance(InjectedExecOps) + inputs.files( + headersDir.file("power_grid_model_c.h"), + headersDir.file("power_grid_model_c/dataset_definitions.h") + ) + outputs.dir(generatedJavaDir) + def jextractExecutables = fileTree(jextractDir) { + include "**/bin/*" + } + doLast { + def jextractBin + if (localJextractBin) { + jextractBin = file(localJextractBin) + } else { + def executableName = osName.contains("windows") + ? "jextract.exe" + : "jextract" + def jextractBins = jextractExecutables.files.findAll() { + it.name == executableName + } + if (jextractBins.size() != 1) { + throw new GradleException( + "Expected exactly one Jextract executable, " + + "but found ${jextractBins.size()}: $jextractBins" + ) + } + jextractBin = jextractBins.first() + } + execOps.execOperations.exec { + commandLine jextractBin, + "--output", generatedJavaDir, + "--target-package", "org.lfenergy.pgm", + "--header-class-name", "PowerGridModelC", + headersDir.file("power_grid_model_c.h").asFile, + headersDir.file( + "power_grid_model_c/dataset_definitions.h" + ).asFile + } + } +} + +sourceSets { + main { + java { + srcDir generatedJavaDir + } + resources { + srcDir nativeLibDir + } + } +} + +tasks.named('compileJava') { + dependsOn(tasks.named('generatePGMBindings')) +} + +tasks.named('processResources') { + dependsOn(tasks.named('generatePGMBindings')) +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = 'lib-power-grid-model' + } + } +} + +dependencies { + testImplementation platform('org.junit:junit-bom:6.0.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation platform('org.mockito:mockito-bom:5.23.0') + testRuntimeOnly 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' +} + +test { + useJUnitPlatform() +} diff --git a/config/checkstyle/checkstyle_main.xml b/config/checkstyle/checkstyle_main.xml new file mode 100644 index 0000000..a64f23d --- /dev/null +++ b/config/checkstyle/checkstyle_main.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/checkstyle_test.xml b/config/checkstyle/checkstyle_test.xml new file mode 100644 index 0000000..25afb75 --- /dev/null +++ b/config/checkstyle/checkstyle_test.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/pmd/ruleset.xml b/config/pmd/ruleset.xml new file mode 100644 index 0000000..e4af2e7 --- /dev/null +++ b/config/pmd/ruleset.xml @@ -0,0 +1,61 @@ + + + + PMD rules for the Dynamic Safety Analyzer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/spotless/code_style.xml b/config/spotless/code_style.xml new file mode 100644 index 0000000..66c6c5b --- /dev/null +++ b/config/spotless/code_style.xml @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ab422a1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +#jextract_bin=fill_in_when_to_use_local_jextract + +#URL Jextract java version 25 +jextract_linux_aarch64_url=https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_linux-aarch64_bin.tar.gz +jextract_linux_x64_url=https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_linux-x64_bin.tar.gz +jextract_macos_aarch64_url=https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_macos-aarch64_bin.tar.gz +jextract_macos_x64_url=https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_macos-x64_bin.tar.gz +jextract_windows_x64_url=https://download.java.net/java/early_access/jextract/25/2/openjdk-25-jextract+2-4_windows-x64_bin.tar.gz diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..61285a6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a351597 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8ca7286 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +rootProject.name = 'libpower-grid-model-java' diff --git a/src/main/java/org/lfenergy/pgm/example/PowerGridModelExample.java b/src/main/java/org/lfenergy/pgm/example/PowerGridModelExample.java new file mode 100644 index 0000000..30fdd5f --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/example/PowerGridModelExample.java @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.example; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; + +import org.lfenergy.pgm.loader.PGMLoader; +import org.lfenergy.pgm.PowerGridModelC; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Java equivalent of power_grid_model_c_example/main.c + *

+ * Network topology: source_0 --node_1---- sym_load_2 | |---- sym_load_3 + *

+ * NOTE: The generated PowerGridModelC_1 class hardcodes the .so path at generation time. If the library has moved, re-run jextract or update SYMBOL_LOOKUP manually. + */ +@SuppressWarnings("PMD.SystemPrintln") +public final class PowerGridModelExample { + + private PowerGridModelExample() { + + // Prevent initialization + } + + static void main() { + + final PGMLoader loader = new PGMLoader(); + + loader.autoload(); + + // when autoload is used, this is not needed, as autoload already performs this check + loader.check(true); + + System.out.println("\nThis is a Java FFM example calling the Power Grid Model C API."); + runExample(); + } + + // ------------------------------------------------------------------------- + + @SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:MethodLength", + "PMD.NcssCount", + "PMD.CognitiveComplexity", + "PMD.VariableDeclarationUsageDistance"}) + private static void runExample() { + + final MemorySegment handle = PowerGridModelC.PGM_create_handle(); + + MemorySegment nodeInput = MemorySegment.NULL; + MemorySegment symLoadInput = MemorySegment.NULL; + MemorySegment inputDataset = MemorySegment.NULL; + MemorySegment model = MemorySegment.NULL; + + MemorySegment nodeOutput = MemorySegment.NULL; + MemorySegment singleOutputDataset = MemorySegment.NULL; + MemorySegment batchOutputDataset = MemorySegment.NULL; + + MemorySegment opt = MemorySegment.NULL; + + MemorySegment sourceUpdate = MemorySegment.NULL; + MemorySegment loadUpdate = MemorySegment.NULL; + MemorySegment batchUpdateDataset = MemorySegment.NULL; + + try (Arena arena = Arena.ofConfined()) { + // ---- create input buffers ---- + nodeInput = PowerGridModelC.PGM_create_buffer(handle, PowerGridModelC.PGM_def_input_node(), 1); + ensureNoError(handle); + + symLoadInput = PowerGridModelC.PGM_create_buffer(handle, PowerGridModelC.PGM_def_input_sym_load(), 2); + ensureNoError(handle); + + final long sourceSize = PowerGridModelC.PGM_meta_component_size(handle, PowerGridModelC.PGM_def_input_source()); + final long sourceAlignment = PowerGridModelC.PGM_meta_component_alignment(handle, PowerGridModelC.PGM_def_input_source()); + final MemorySegment sourceInput = arena.allocate(sourceSize, sourceAlignment); + + // ---- assign input attributes ---- + final long nodeIdOffset = PowerGridModelC.PGM_meta_attribute_offset(handle, PowerGridModelC.PGM_def_input_node_id()); + final long nodeURatedOffset = PowerGridModelC.PGM_meta_attribute_offset(handle, PowerGridModelC.PGM_def_input_node_u_rated()); + nodeInput.set(PowerGridModelC.PGM_ID, nodeIdOffset, 1); + nodeInput.set(PowerGridModelC.C_DOUBLE, nodeURatedOffset, 10e3); + + PowerGridModelC.PGM_buffer_set_nan(handle, PowerGridModelC.PGM_def_input_source(), sourceInput, 0, 1); + + final MemorySegment sourceId = arena.allocate(PowerGridModelC.PGM_ID); + sourceId.set(PowerGridModelC.PGM_ID, 0, 0); + + final MemorySegment node = arena.allocate(PowerGridModelC.PGM_ID); + node.set(PowerGridModelC.PGM_ID, 0, 1); + + final MemorySegment status = arena.allocate(PowerGridModelC.C_CHAR); + status.set(PowerGridModelC.C_CHAR, 0, (byte) 1); + + final MemorySegment uRef = arena.allocate(PowerGridModelC.C_DOUBLE); + uRef.set(PowerGridModelC.C_DOUBLE, 0, 1.0); + + final MemorySegment sk = arena.allocate(PowerGridModelC.C_DOUBLE); + sk.set(PowerGridModelC.C_DOUBLE, 0, 1e6); + + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_source_id(), sourceInput, sourceId, 0, 1, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_source_node(), sourceInput, node, 0, 1, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_source_status(), sourceInput, status, 0, 1, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_source_u_ref(), sourceInput, uRef, 0, 1, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_source_sk(), sourceInput, sk, 0, 1, -1); + ensureNoError(handle); + + final int[] symLoadId = {2, 3}; + final MemorySegment symLoadIdSeg = toNativeIntArray(arena, symLoadId); + + final MemorySegment loadType = arena.allocate(PowerGridModelC.C_CHAR); + loadType.set(PowerGridModelC.C_CHAR, 0, (byte) 0); + + final double[] pqSpecified = {50e3, 10e3, 100e3, 20e3}; + final MemorySegment pqSpecifiedSeg = toNativeDoubleArray(arena, pqSpecified); + + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_id(), symLoadInput, symLoadIdSeg, 0, 2, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_node(), symLoadInput, node, 0, 2, 0); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_status(), symLoadInput, status, 0, 2, 0); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_type(), symLoadInput, loadType, 0, 2, 0); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_p_specified(), symLoadInput, pqSpecifiedSeg, 0, 2, 16); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_input_sym_load_q_specified(), symLoadInput, pqSpecifiedSeg.asSlice(Double.BYTES), 0, 2, 16); + ensureNoError(handle); + + // ---- initialize model ---- + inputDataset = PowerGridModelC.PGM_create_dataset_const(handle, arena.allocateFrom("input"), 0, 1); + PowerGridModelC.PGM_dataset_const_add_buffer(handle, inputDataset, arena.allocateFrom("node"), 1, 1, MemorySegment.NULL, nodeInput); + PowerGridModelC.PGM_dataset_const_add_buffer(handle, inputDataset, arena.allocateFrom("source"), 1, 1, MemorySegment.NULL, sourceInput); + PowerGridModelC.PGM_dataset_const_add_buffer(handle, inputDataset, arena.allocateFrom("sym_load"), 2, 2, MemorySegment.NULL, symLoadInput); + ensureNoError(handle); + + model = PowerGridModelC.PGM_create_model(handle, 50.0, inputDataset); + ensureNoError(handle); + + // ---- create output buffers ---- + nodeOutput = PowerGridModelC.PGM_create_buffer(handle, PowerGridModelC.PGM_def_sym_output_node(), 3); + ensureNoError(handle); + + final MemorySegment uPuSeg = arena.allocate(3L * PowerGridModelC.C_DOUBLE.byteSize(), PowerGridModelC.C_DOUBLE.byteAlignment()); + final MemorySegment uAngleSeg = arena.allocate(3L * PowerGridModelC.C_DOUBLE.byteSize(), PowerGridModelC.C_DOUBLE.byteAlignment()); + + singleOutputDataset = PowerGridModelC.PGM_create_dataset_mutable(handle, arena.allocateFrom("sym_output"), 0, 1); + PowerGridModelC.PGM_dataset_mutable_add_buffer(handle, singleOutputDataset, arena.allocateFrom("node"), 1, 1, MemorySegment.NULL, nodeOutput); + ensureNoError(handle); + + batchOutputDataset = PowerGridModelC.PGM_create_dataset_mutable(handle, arena.allocateFrom("sym_output"), 1, 3); + PowerGridModelC.PGM_dataset_mutable_add_buffer(handle, batchOutputDataset, arena.allocateFrom("node"), 1, 3, MemorySegment.NULL, nodeOutput); + ensureNoError(handle); + + // ---- one-time calculation ---- + opt = PowerGridModelC.PGM_create_options(handle); + PowerGridModelC.PGM_calculate(handle, model, opt, singleOutputDataset, MemorySegment.NULL); + ensureNoError(handle); + + PowerGridModelC.PGM_buffer_get_value(handle, PowerGridModelC.PGM_def_sym_output_node_u_pu(), nodeOutput, uPuSeg, 0, 1, -1); + PowerGridModelC.PGM_buffer_get_value(handle, PowerGridModelC.PGM_def_sym_output_node_u_angle(), nodeOutput, uAngleSeg, 0, 1, -1); + System.out.println("\nOne-time Calculation"); + System.out.printf("Node result u_pu: %f, u_angle: %f%n", uPuSeg.getAtIndex(PowerGridModelC.C_DOUBLE, 0), uAngleSeg.getAtIndex(PowerGridModelC.C_DOUBLE, 0)); + + // ---- one-time calculation error ---- + PowerGridModelC.PGM_set_max_iter(handle, opt, 1); + PowerGridModelC.PGM_calculate(handle, model, opt, singleOutputDataset, MemorySegment.NULL); + if (PowerGridModelC.PGM_error_code(handle) == PowerGridModelC.PGM_no_error()) { + throw new IllegalStateException("Expected one-time calculation to fail with max_iter=1"); + } + System.out.println("\nOne-time Calculation Error"); + System.out.printf("Error code: %d, error message: %s%n", PowerGridModelC.PGM_error_code(handle), cString(PowerGridModelC.PGM_error_message(handle))); + PowerGridModelC.PGM_set_max_iter(handle, opt, 20); + PowerGridModelC.PGM_clear_error(handle); + + // ---- prepare batch update dataset ---- + sourceUpdate = PowerGridModelC.PGM_create_buffer(handle, PowerGridModelC.PGM_def_update_source(), 3); + ensureNoError(handle); + PowerGridModelC.PGM_buffer_set_nan(handle, PowerGridModelC.PGM_def_update_source(), sourceUpdate, 0, 3); + + final double[] uRefUpdate = {0.95, 1.05, 1.1}; + final MemorySegment uRefUpdateSeg = toNativeDoubleArray(arena, uRefUpdate); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_source_id(), sourceUpdate, sourceId, 0, 3, 0); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_source_u_ref(), sourceUpdate, uRefUpdateSeg, 0, 3, -1); + + loadUpdate = PowerGridModelC.PGM_create_buffer(handle, PowerGridModelC.PGM_def_update_sym_load(), 4); + PowerGridModelC.PGM_buffer_set_nan(handle, PowerGridModelC.PGM_def_update_sym_load(), loadUpdate, 0, 4); + + final int[] loadUpdateId = {2, 3, 2, 3}; + final double[] pUpdate = {100e3, 200e3, 0.0, -200e3}; + final long[] indptrLoad = {0, 2, 3, 4}; + final MemorySegment loadUpdateIdSeg = toNativeIntArray(arena, loadUpdateId); + final MemorySegment pUpdateSeg = toNativeDoubleArray(arena, pUpdate); + final MemorySegment indptrLoadSeg = toNativeLongArray(arena, indptrLoad); + + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_sym_load_id(), loadUpdate, loadUpdateIdSeg, 0, 4, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_sym_load_p_specified(), loadUpdate, pUpdateSeg, 0, 4, -1); + + batchUpdateDataset = PowerGridModelC.PGM_create_dataset_const(handle, arena.allocateFrom("update"), 1, 3); + PowerGridModelC.PGM_dataset_const_add_buffer(handle, batchUpdateDataset, arena.allocateFrom("source"), 1, 3, MemorySegment.NULL, sourceUpdate); + PowerGridModelC.PGM_dataset_const_add_buffer(handle, batchUpdateDataset, arena.allocateFrom("sym_load"), -1, 4, indptrLoadSeg, loadUpdate); + ensureNoError(handle); + + // ---- batch calculation ---- + PowerGridModelC.PGM_calculate(handle, model, opt, batchOutputDataset, batchUpdateDataset); + ensureNoError(handle); + + PowerGridModelC.PGM_buffer_get_value(handle, PowerGridModelC.PGM_def_sym_output_node_u_pu(), nodeOutput, uPuSeg, 0, 3, -1); + PowerGridModelC.PGM_buffer_get_value(handle, PowerGridModelC.PGM_def_sym_output_node_u_angle(), nodeOutput, uAngleSeg, 0, 3, -1); + System.out.println("\nBatch Calculation"); + for (int i = 0; i < 3; i++) { + System.out.printf("Scenario %d, u_pu: %f, u_angle: %f%n", i, uPuSeg.getAtIndex(PowerGridModelC.C_DOUBLE, i), uAngleSeg.getAtIndex(PowerGridModelC.C_DOUBLE, i)); + } + + // ---- batch calculation error ---- + pUpdate[2] = 100e12; + loadUpdateId[3] = 100; + pUpdateSeg.setAtIndex(PowerGridModelC.C_DOUBLE, 2, pUpdate[2]); + loadUpdateIdSeg.setAtIndex(PowerGridModelC.C_INT, 3, loadUpdateId[3]); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_sym_load_id(), loadUpdate, loadUpdateIdSeg, 0, 4, -1); + PowerGridModelC.PGM_buffer_set_value(handle, PowerGridModelC.PGM_def_update_sym_load_p_specified(), loadUpdate, pUpdateSeg, 0, 4, -1); + + PowerGridModelC.PGM_calculate(handle, model, opt, batchOutputDataset, batchUpdateDataset); + if (PowerGridModelC.PGM_error_code(handle) == PowerGridModelC.PGM_no_error()) { + throw new IllegalStateException("Expected batch calculation to fail for invalid update data"); + } + + System.out.println("\nBatch Calculation Error"); + System.out.printf("Error code: %d%n", PowerGridModelC.PGM_error_code(handle)); + + final long nFailedScenarios = PowerGridModelC.PGM_n_failed_scenarios(handle); + final MemorySegment failedScenarios = PowerGridModelC.PGM_failed_scenarios(handle); + final MemorySegment batchErrs = PowerGridModelC.PGM_batch_errors(handle); + for (int i = 0; i < nFailedScenarios; i++) { + final long failedScenario = failedScenarios.getAtIndex(PowerGridModelC.PGM_Idx, i); + final MemorySegment errorPtr = batchErrs.getAtIndex(PowerGridModelC.C_POINTER, i); + System.out.printf("Failed scenario %d, error message: %s%n", failedScenario, cString(errorPtr)); + } + + System.out.println("Normal result:"); + System.out.printf("Scenario 0, u_pu: %f, u_angle: %f%n", uPuSeg.getAtIndex(PowerGridModelC.C_DOUBLE, 0), uAngleSeg.getAtIndex(PowerGridModelC.C_DOUBLE, 0)); + PowerGridModelC.PGM_clear_error(handle); + } finally { + if (!batchUpdateDataset.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_dataset_const(batchUpdateDataset); + } + if (!loadUpdate.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_buffer(loadUpdate); + } + if (!sourceUpdate.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_buffer(sourceUpdate); + } + if (!opt.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_options(opt); + } + if (!nodeOutput.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_buffer(nodeOutput); + } + if (!batchOutputDataset.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_dataset_mutable(batchOutputDataset); + } + if (!singleOutputDataset.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_dataset_mutable(singleOutputDataset); + } + if (!model.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_model(model); + } + if (!inputDataset.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_dataset_const(inputDataset); + } + if (!symLoadInput.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_buffer(symLoadInput); + } + if (!nodeInput.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_buffer(nodeInput); + } + if (!handle.equals(MemorySegment.NULL)) { + PowerGridModelC.PGM_destroy_handle(handle); + } + } + } + + private static void ensureNoError(MemorySegment handle) { + + final long errorCode = PowerGridModelC.PGM_error_code(handle); + if (errorCode != PowerGridModelC.PGM_no_error()) { + throw new IllegalStateException( + "PGM error " + errorCode + ": " + cString(PowerGridModelC.PGM_error_message(handle)) + ); + } + } + + private static String cString(MemorySegment cStringPtr) { + + if (cStringPtr.equals(MemorySegment.NULL)) { + return ""; + } + return cStringPtr.getString(0, UTF_8); + } + + private static MemorySegment toNativeIntArray(Arena arena, + int... values) { + + final MemorySegment segment = arena.allocate(values.length * PowerGridModelC.C_INT.byteSize(), PowerGridModelC.C_INT.byteAlignment()); + for (int i = 0; i < values.length; i++) { + segment.setAtIndex(PowerGridModelC.C_INT, i, values[i]); + } + return segment; + } + + private static MemorySegment toNativeDoubleArray(Arena arena, + double... values) { + + final MemorySegment segment = arena.allocate(values.length * PowerGridModelC.C_DOUBLE.byteSize(), PowerGridModelC.C_DOUBLE.byteAlignment()); + for (int i = 0; i < values.length; i++) { + segment.setAtIndex(PowerGridModelC.C_DOUBLE, i, values[i]); + } + return segment; + } + + private static MemorySegment toNativeLongArray(Arena arena, + long... values) { + + final MemorySegment segment = arena.allocate(values.length * PowerGridModelC.C_LONG_LONG.byteSize(), PowerGridModelC.C_LONG_LONG.byteAlignment()); + for (int i = 0; i < values.length; i++) { + segment.setAtIndex(PowerGridModelC.C_LONG_LONG, i, values[i]); + } + return segment; + } +} diff --git a/src/main/java/org/lfenergy/pgm/loader/Architecture.java b/src/main/java/org/lfenergy/pgm/loader/Architecture.java new file mode 100644 index 0000000..66fe587 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/Architecture.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +enum Architecture { + X86_64, + ARM64, + UNKNOWN +} diff --git a/src/main/java/org/lfenergy/pgm/loader/OperatingSystem.java b/src/main/java/org/lfenergy/pgm/loader/OperatingSystem.java new file mode 100644 index 0000000..66fb255 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/OperatingSystem.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +enum OperatingSystem { + MACOS, + LINUX, + WINDOWS, + UNKNOWN +} diff --git a/src/main/java/org/lfenergy/pgm/loader/PGMLoader.java b/src/main/java/org/lfenergy/pgm/loader/PGMLoader.java new file mode 100644 index 0000000..713f7a7 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/PGMLoader.java @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.lfenergy.pgm.loader.Architecture.ARM64; +import static org.lfenergy.pgm.loader.OperatingSystem.WINDOWS; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.lfenergy.pgm.PowerGridModelC; + +public class PGMLoader { + + private final PlatformDetector platformDetector; + private final ResourceLibraryLoader resourceLibraryLoader; + private final PGMInvoker pgmInvoker; + + // Thread-safe state + private final AtomicReference pgmBuildVersion = new AtomicReference<>(); + private final AtomicReference pgmRuntimeVersion = new AtomicReference<>(); + private final AtomicBoolean loaded = new AtomicBoolean(false); + + public PGMLoader() { + + this(new PlatformDetector(), new ResourceLibraryLoader(), PowerGridModelC::PGM_version); + } + + PGMLoader(PlatformDetector platformDetector, + ResourceLibraryLoader resourceLibraryLoader, + PGMInvoker pgmInvoker) { + + this.platformDetector = platformDetector; + this.resourceLibraryLoader = resourceLibraryLoader; + this.pgmInvoker = pgmInvoker; + } + + /** + * Resolves and loads the Power Grid Model native library exactly once, + * regardless of how many times this method is called. Subsequent calls are + * no-ops and return immediately. + *

+ * This is a convenience alternative to the explicit two-step pattern: + *

{@code
+     * String path = NativeLibraryResolver.resolveNativeLibraryPath(PowerGridModelC.class);
+     * System.load(path);
+     * }
+ * + * @throws IllegalStateException if the native library cannot be found + * @throws RuntimeException if the library resource cannot be extracted + */ + public void autoload() { + + if (loaded.compareAndSet(false, true)) { + // Detect platform + final Platform platform = platformDetector.detectPlatform(); + + // Determine resource path for library + final String libraryResourcePath = determineLibraryResourcePath(platform); + + // Load library + resourceLibraryLoader.loadResourceLibrary(libraryResourcePath); + + // Ensure it was loaded correctly + check(true); + } + } + + /** + * Checks if the native PGM library is loaded. Optionally also checks if the build version equals the runtime version. + * + * @param matchVersion when set to true, an exception is thrown when the PGM is loaded, but the build and runtime versions don't match + * + * @throws PGMLoaderException thrown when the native PGM library is not loaded + * @throws VersionMismatchException thrown when {@code matchVersion} is true and the build and runtime versions don't match + */ + public void check(boolean matchVersion) { + + final String buildVersion = getPGMBuildVersion(); + final String runtimeVersion = getPGMRuntimeVersion(); + + if (matchVersion && !runtimeVersion.equals(buildVersion)) { + throw new VersionMismatchException(runtimeVersion, buildVersion); + } + } + + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public String getPGMRuntimeVersion() { + + if (pgmRuntimeVersion.get() != null) { + return pgmRuntimeVersion.get(); + } + + try { + pgmRuntimeVersion.set(pgmInvoker.PGM_version().getString(0, UTF_8)); + } catch (Throwable t) { + throw new PGMLoaderException("PGM library is not loaded", t); + } + + return pgmRuntimeVersion.get(); + } + + public String getPGMBuildVersion() { + + if (pgmBuildVersion.get() != null) { + return pgmBuildVersion.get(); + } + + final InputStream ris = getClass().getResourceAsStream("/version"); + if (ris == null) { + throw new PGMLoaderException("Failed to fetch build version from resource file"); + } + + try (InputStream is = new BufferedInputStream(ris)) { + pgmBuildVersion.set(new String(is.readAllBytes(), UTF_8)); + } catch (IOException e) { + throw new PGMLoaderException("Failed to fetch build version from resource file", e); + } + + return pgmBuildVersion.get(); + } + + private String determineLibraryResourcePath(Platform platform) { + + final String archPart = switch (platform.architecture()) { + case ARM64 -> "arm64"; + case X86_64 -> "x86_64"; + default -> throw new PGMLoaderException("Unsupported CPU architecture"); + }; + + final String osPart = switch (platform.operatingSystem()) { + case MACOS -> "macosx.dylib"; + case LINUX -> "linux.so"; + case WINDOWS -> "windows.dll"; + default -> throw new PGMLoaderException("Unsupported CPU architecture"); + }; + + if (platform.architecture() == ARM64 && platform.operatingSystem() == WINDOWS) { + throw new PGMLoaderException("Windows ARM64 is not supported"); + } + + return String.format("power_grid_model_c_%s_%s", archPart, osPart); + } + + /** + * Interface that allows for mocking in unit tests. + */ + @SuppressWarnings({"checkstyle:MethodName", "PMD.MethodNamingConventions"}) + @FunctionalInterface + interface PGMInvoker { + + MemorySegment PGM_version(); + } +} diff --git a/src/main/java/org/lfenergy/pgm/loader/PGMLoaderException.java b/src/main/java/org/lfenergy/pgm/loader/PGMLoaderException.java new file mode 100644 index 0000000..af2121a --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/PGMLoaderException.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import java.io.Serial; + +public class PGMLoaderException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -4755108178968242882L; + + PGMLoaderException(String message) { + + super(message); + } + + PGMLoaderException(String message, + Throwable cause) { + + super(message, cause); + } +} diff --git a/src/main/java/org/lfenergy/pgm/loader/Platform.java b/src/main/java/org/lfenergy/pgm/loader/Platform.java new file mode 100644 index 0000000..79b7e19 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/Platform.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +record Platform(OperatingSystem operatingSystem, Architecture architecture) { + +} diff --git a/src/main/java/org/lfenergy/pgm/loader/PlatformDetector.java b/src/main/java/org/lfenergy/pgm/loader/PlatformDetector.java new file mode 100644 index 0000000..0b0b779 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/PlatformDetector.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +class PlatformDetector { + + private final SystemPropertyProvider systemPropertyProvider; + + PlatformDetector() { + + this(new SystemPropertyProvider() { + + @Override + public String getOsArch() { + + return System.getProperty("os.arch", ""); + } + + @Override + public String getOsName() { + + return System.getProperty("os.name", ""); + } + }); + } + + PlatformDetector(SystemPropertyProvider systemPropertyProvider) { + + this.systemPropertyProvider = systemPropertyProvider; + } + + Platform detectPlatform() { + + final OperatingSystem operatingSystem = detectOperatingSystem(systemPropertyProvider.getOsName()); + final Architecture architecture = detectArchitecture(systemPropertyProvider.getOsArch()); + + return new Platform(operatingSystem, architecture); + } + + private Architecture detectArchitecture(final String osArch) { + + final String normalized = osArch == null ? "" : osArch.toLowerCase(); + + return switch (normalized) { + case "aarch64", "arm64" -> Architecture.ARM64; + case "amd64", "x86_64" -> Architecture.X86_64; + default -> Architecture.UNKNOWN; + }; + } + + private OperatingSystem detectOperatingSystem(final String osName) { + + final String normalized = osName == null ? "" : osName.toLowerCase(); + + if (normalized.contains("mac")) { + return OperatingSystem.MACOS; + } + if (normalized.contains("linux")) { + return OperatingSystem.LINUX; + } + if (normalized.contains("win")) { + return OperatingSystem.WINDOWS; + } + return OperatingSystem.UNKNOWN; + } + + /** + * Interface that allows for mocking in unit tests. + */ + interface SystemPropertyProvider { + + String getOsArch(); + + String getOsName(); + } +} diff --git a/src/main/java/org/lfenergy/pgm/loader/ResourceLibraryLoader.java b/src/main/java/org/lfenergy/pgm/loader/ResourceLibraryLoader.java new file mode 100644 index 0000000..c132cee --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/ResourceLibraryLoader.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +final class ResourceLibraryLoader { + + private final Dependencies dependencies; + + ResourceLibraryLoader() { + + this( + new Dependencies() { + + @Override + public void loadNativeLibrary(final String nativeLibraryPath) { + + System.load(nativeLibraryPath); + } + + @Override + public URL openResource(String path) { + + return Thread.currentThread().getContextClassLoader().getResource(path); + } + + @Override + public boolean resourceIsFile(URL resourceURL) { + + return "file".equals(resourceURL.getProtocol()); + } + + @Override + public Path copyIntoTemporaryFile(final InputStream from, + final String prefix, + final String suffix) throws IOException { + + final Path tempFile = Files.createTempFile(prefix, suffix); + tempFile.toFile().deleteOnExit(); + + Files.copy(from, tempFile, StandardCopyOption.REPLACE_EXISTING); + + return tempFile; + } + } + ); + } + + ResourceLibraryLoader( + final Dependencies dependencies + ) { + + this.dependencies = dependencies; + } + + /** + * Looks up the native library at the provided path, and loads it using {@link System#load(String)}. + *

+ * If the resource URL shows the native library is available as file, it loads the native library directly from that file. In other cases, it + * first copies the native library to a temporary file, creates a shutdown hook to delete it on shutdown, then loads the file from the temporary + * file path. + *

+ * @throws PGMLoaderException if the resource file containing the native library cannot be found or opened, or if something unexpected happens + * when copying the native library to a temporary file. + */ + void loadResourceLibrary(final String resourceLibraryPath) { + + final Path libraryPath = provideLoadableLibraryFromResources(resourceLibraryPath); + dependencies.loadNativeLibrary(libraryPath.toString()); + } + + /** + * Looks up the native library at the provided path, and ensures it is available for loading. Returns a path to a file containing the native + * library that can be loaded by {@link System#load(String)}. + *

+ * If the resource URL shows the native library is available as file, simply returns the path of the file. In other cases, it copies the native + * library to a temporary file, and creates a shutdown hook to delete it on shutdown. + *

+ * @return a path to a file containing the native library that can be loaded by {@link System#load(String)}. + * @throws PGMLoaderException if the resource file containing the native library cannot be found or opened, or if something unexpected happens + * when copying the native library to a temporary file. + */ + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private Path provideLoadableLibraryFromResources(final String resourceLibraryPath) { + + final URL resourceUrl = dependencies.openResource(resourceLibraryPath); + if (resourceUrl == null) { + throw new PGMLoaderException("Failed to find packaged native library"); + } + + try { + // Shortcut for situation common in local development (no jar-file) + if (dependencies.resourceIsFile(resourceUrl)) { + return Path.of(resourceUrl.toURI()); + } + + try (InputStream in = new BufferedInputStream(resourceUrl.openStream())) { + return dependencies.copyIntoTemporaryFile( + in, "pgm-native-", resourceLibraryPath.replace('.', '-') + ); + } + } catch (Exception e) { + throw new PGMLoaderException("Failed to open packaged native library", e); + } + } + + /** + * Interface that allows for mocking in unit tests. + */ + interface Dependencies { + + void loadNativeLibrary(String path); + + URL openResource(String path); + + boolean resourceIsFile(URL resourceUrl); + + Path copyIntoTemporaryFile(InputStream from, + String prefix, + String suffix) throws IOException; + } +} diff --git a/src/main/java/org/lfenergy/pgm/loader/VersionMismatchException.java b/src/main/java/org/lfenergy/pgm/loader/VersionMismatchException.java new file mode 100644 index 0000000..8d7be45 --- /dev/null +++ b/src/main/java/org/lfenergy/pgm/loader/VersionMismatchException.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import java.io.Serial; + +public class VersionMismatchException extends PGMLoaderException { + + @Serial + private static final long serialVersionUID = 4716888612549282053L; + + private final String runtimeVersion; + private final String buildVersion; + + public VersionMismatchException(String runtimeVersion, + String buildVersion) { + + super(String.format("libpower-grid-model-java was built for version %s, but version %s is present at runtime", buildVersion, runtimeVersion)); + this.runtimeVersion = runtimeVersion; + this.buildVersion = buildVersion; + } + + public String getRuntimeVersion() { + + return runtimeVersion; + } + + public String getBuildVersion() { + + return buildVersion; + } +} diff --git a/src/test/java/org/lfenergy/pgm/loader/PGMLoaderTest.java b/src/test/java/org/lfenergy/pgm/loader/PGMLoaderTest.java new file mode 100644 index 0000000..03fe468 --- /dev/null +++ b/src/test/java/org/lfenergy/pgm/loader/PGMLoaderTest.java @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; +import static org.lfenergy.pgm.loader.Architecture.*; +import static org.lfenergy.pgm.loader.OperatingSystem.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.lfenergy.pgm.loader.PGMLoader.PGMInvoker; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class PGMLoaderTest { + + @Mock + private PlatformDetector platformDetector; + + @Mock + private ResourceLibraryLoader resourceLibraryLoader; + + @Mock + private PGMInvoker pgmInvoker; + + @InjectMocks + private PGMLoader pgmLoader; + + @Captor + private ArgumentCaptor resourceLibraryLoaderCaptor; + + private static String pgmBuildVersion; + private static MemorySegment pgmBuildVersionPointer; + + @BeforeAll + static void beforeAll() { + + final InputStream ris = PGMLoader.class.getResourceAsStream("/version"); + + try (InputStream is = new BufferedInputStream(ris)) { + pgmBuildVersion = new String(is.readAllBytes(), UTF_8); + pgmBuildVersionPointer = createNullTerminatedString(pgmBuildVersion); + } catch (IOException e) { + throw new RuntimeException("Unexpected: failed to read version resource", e); + } + } + + @MethodSource("testAutoloadProvider") + @ParameterizedTest + void testAutoload(Platform platform, + String expectedResourceFile) { + + // Stub the static method + when(pgmInvoker.PGM_version()) + .thenReturn(pgmBuildVersionPointer); + + when(platformDetector.detectPlatform()).thenReturn(platform); + + pgmLoader.autoload(); + + // Second call to test the caching (it should only call the PlatformDetector and ResourceLibraryLoader once) + pgmLoader.autoload(); + + verify(platformDetector, times(1)).detectPlatform(); + verify(resourceLibraryLoader, times(1)).loadResourceLibrary(resourceLibraryLoaderCaptor.capture()); + + assertEquals(expectedResourceFile, resourceLibraryLoaderCaptor.getValue()); + } + + // spotless:off + static Stream testAutoloadProvider() { + + return Stream.of( + argumentSet( + "MACOS, ARM64", + new Platform(MACOS, ARM64), + "power_grid_model_c_arm64_macosx.dylib" + ), argumentSet( + "MACOS, X86_64", + new Platform(MACOS, X86_64), + "power_grid_model_c_x86_64_macosx.dylib" + ), argumentSet( + "LINUX, ARM64", + new Platform(LINUX, ARM64), + "power_grid_model_c_arm64_linux.so" + ), argumentSet( + "LINUX, X86_64", + new Platform(LINUX, X86_64), + "power_grid_model_c_x86_64_linux.so" + ), argumentSet( + "WINDOWS, X86_64", + new Platform(WINDOWS, X86_64), + "power_grid_model_c_x86_64_windows.dll" + ) + ); + } + // spotless:on + + @Test + void testCheckThrowsVersionMismatchExceptionWhenCheckingVersionAndVersionsNotEqual() { + + when(pgmInvoker.PGM_version()) + .thenReturn(createNullTerminatedString("This is not a valid PGM version")); + + // Should throw + assertThrows(VersionMismatchException.class, () -> pgmLoader.check(true)); + } + + @Test + void testCheckDoesNotThrowWhenCheckingVersionAndVersionsEqual() { + + when(pgmInvoker.PGM_version()) + .thenReturn(pgmBuildVersionPointer); + + // Should not throw + assertDoesNotThrow(() -> pgmLoader.check(true)); + } + + @Test + void testCheckDoesNotThrowWhenNotCheckingVersionAndVersionsNotEqual() { + + when(pgmInvoker.PGM_version()) + .thenReturn(createNullTerminatedString("This is not a valid PGM version")); + + // Should not throw + assertDoesNotThrow(() -> pgmLoader.check(false)); + } + + private static MemorySegment createNullTerminatedString(String string) { + + ByteBuffer buffer = UTF_8.encode(string); + byte[] bytes = new byte[buffer.remaining() + 1]; + buffer.get(bytes, 0, buffer.remaining()); + bytes[bytes.length - 1] = (byte) 0; + return MemorySegment.ofArray(bytes); + } +} diff --git a/src/test/java/org/lfenergy/pgm/loader/PlatformDetectorTest.java b/src/test/java/org/lfenergy/pgm/loader/PlatformDetectorTest.java new file mode 100644 index 0000000..4503f64 --- /dev/null +++ b/src/test/java/org/lfenergy/pgm/loader/PlatformDetectorTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.lfenergy.pgm.loader.Architecture.*; +import static org.lfenergy.pgm.loader.OperatingSystem.*; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PlatformDetectorTest { + + @Mock + private PlatformDetector.SystemPropertyProvider systemPropertyProvider; + + @InjectMocks + private PlatformDetector platformDetector; + + @MethodSource("testProvider") + @ParameterizedTest + void test(String osName, + String osArch, + Platform expectedPlatform) { + + when(systemPropertyProvider.getOsName()).thenReturn(osName); + when(systemPropertyProvider.getOsArch()).thenReturn(osArch); + + final Platform actualPlatform = platformDetector.detectPlatform(); + + assertEquals(expectedPlatform, actualPlatform); + } + + // spotless:off + static Stream testProvider() { + + return Stream.of( + // + // CPU: Apple M4 Pro (ARMv9.2-A) + // OS: Mac OS X Tahoe 26.5.2 + // Expected platform: MACOS, ARM64 + Arguments.argumentSet( + "MACOS, ARM64", + "Mac OS X", "aarch64", + new Platform(MACOS, ARM64) + ), + // + // CPU: AMD Ryzen 7 6800U (x86-64) + // OS: Arch Linux + // Expected platform: LINUX, X86_64 + Arguments.argumentSet( + "LINUX, X86_64", + "Linux", "amd64", + new Platform(LINUX, X86_64) + ), + // + // CPU: Rockchip RK3399 (ARMv8-A) + // OS: Manjaro ARM + // Expected platform: LINUX, ARM64 + Arguments.argumentSet( + "LINUX, ARM64", + "Linux", "aarch64", + new Platform(LINUX, ARM64) + ), + // + // CPU: Intel Core i7 ... + // OS: Windows 11 + // Expected platform: Windows, X86_64 + Arguments.argumentSet( + "WINDOWS, X86_64", + "Windows 11", "amd64", + new Platform(WINDOWS, X86_64) + ) + ); + } + // spotless:on +} diff --git a/src/test/java/org/lfenergy/pgm/loader/ResourceLibraryLoaderTest.java b/src/test/java/org/lfenergy/pgm/loader/ResourceLibraryLoaderTest.java new file mode 100644 index 0000000..e9c4503 --- /dev/null +++ b/src/test/java/org/lfenergy/pgm/loader/ResourceLibraryLoaderTest.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// SPDX-License-Identifier: MPL-2.0 + +package org.lfenergy.pgm.loader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URL; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.lfenergy.pgm.PowerGridModelC; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ResourceLibraryLoaderTest { + + @Mock + private ResourceLibraryLoader.Dependencies dependencies; + + @InjectMocks + private ResourceLibraryLoader resourceLibraryLoader; + + @Captor + private ArgumentCaptor systemLoadCaptor; + + /** + * Checks if the resource file exists. + */ + @ParameterizedTest + @MethodSource("testProvider") + void resourceFileExists(String resourceLibraryPath) { + + assertNotNull(PowerGridModelC.class.getResource(resourceLibraryPath)); + } + + /** + * Tests loading the native PGM library from the resources when the resource is not a file on the filesystem, but e.g. part of the jar (which is + * a zip-file). In those cases, it should copy the resource file to a temporary file, and load it from there. + */ + @ParameterizedTest + @MethodSource("testProvider") + void testWhenResourceIsFile(String resourceLibraryPath) throws Exception { + + final URL resourceURL = PowerGridModelC.class.getResource(resourceLibraryPath); + + when(dependencies.openResource(resourceLibraryPath)) + .thenReturn(resourceURL); + + when(dependencies.resourceIsFile(resourceURL)) + .thenReturn(true); + + resourceLibraryLoader.loadResourceLibrary(resourceLibraryPath); + + verify(dependencies, times(1)).openResource(resourceLibraryPath); + verify(dependencies, times(1)).resourceIsFile(resourceURL); + verify(dependencies, never()).copyIntoTemporaryFile(any(), any(), any()); + verify(dependencies, times(1)).loadNativeLibrary(systemLoadCaptor.capture()); + assertEquals(resourceURL.getFile(), systemLoadCaptor.getValue()); + } + + /** + * Tests loading the native PGM library from the resources when the resource is a file on the filesystem, which is usually the case during + * development (when deployed, the resource file is an entry in the zip-file). When the resource is a file on the filesystem, it should load the + * file directly, and not first copy it to a temporary file. + */ + @ParameterizedTest + @MethodSource("testProvider") + void testWhenResourceIsNotFile(String resourceLibraryPath) throws Exception { + + final String expectedPathString = "/foo/bar"; + final Path expectedPath = Path.of(expectedPathString); + final URL resourceURL = PowerGridModelC.class.getResource(resourceLibraryPath); + + when(dependencies.copyIntoTemporaryFile(any(), any(), any())) + .thenReturn(expectedPath); + + when(dependencies.openResource(resourceLibraryPath)) + .thenReturn(resourceURL); + + when(dependencies.resourceIsFile(resourceURL)) + .thenReturn(false); + + resourceLibraryLoader.loadResourceLibrary(resourceLibraryPath); + + verify(dependencies, times(1)).openResource(resourceLibraryPath); + verify(dependencies, times(1)).resourceIsFile(resourceURL); + verify(dependencies, times(1)).copyIntoTemporaryFile(any(), any(), any()); + verify(dependencies, times(1)).loadNativeLibrary(systemLoadCaptor.capture()); + assertEquals(expectedPathString, systemLoadCaptor.getValue()); + } + + // spotless:off + static Stream testProvider() { + + return Stream.of( + Arguments.argumentSet("LINUX, ARM64", "/power_grid_model_c_arm64_linux.so"), + Arguments.argumentSet("LINUX, X86_64", "/power_grid_model_c_x86_64_linux.so"), + Arguments.argumentSet("MACOS, ARM64", "/power_grid_model_c_arm64_macosx.dylib"), + Arguments.argumentSet("MACOS, X86_64", "/power_grid_model_c_x86_64_macosx.dylib"), + Arguments.argumentSet("WINDOWS, X86_64", "/power_grid_model_c_x86_64_windows.dll") + ); + } + // spotless:on +}