From 65f79929fcabb04c9b65343b286f84912502d7cd Mon Sep 17 00:00:00 2001 From: JKRT Date: Mon, 11 May 2026 10:14:51 +0200 Subject: [PATCH 1/2] Fix matrix read/write update to api --- LICENSE.md | 2 +- Project.toml | 2 +- src/api.jl | 96 ++++++++++++++++++++++++++++++++++++++---------- test/runtests.jl | 37 +++++++++++++++++++ 4 files changed, 116 insertions(+), 21 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 6aa1d87..73c3fa9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,7 +3,7 @@ /* * This file is part of OpenModelica. * - * Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), + * Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC), * c/o Linköpings universitet, Department of Computer and Information Science, * SE-58183 Linköping, Sweden. * diff --git a/Project.toml b/Project.toml index 4de8226..b1fae66 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "OMRuntimeExternalC" uuid = "ada38df0-e20e-11ed-02f3-2b4b19c1ec8a" authors = ["Adrian Pop ", "John Tinnerholm "] -version = "0.3.0" +version = "0.3.1" [deps] CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" diff --git a/src/api.jl b/src/api.jl index 5028ab0..aa6787d 100644 --- a/src/api.jl +++ b/src/api.jl @@ -85,18 +85,19 @@ function ModelicaStandardTables_CombiTable1D_init2( fileName::String, tableName::String, table::Matrix{Float64}, - nRow::Int64, - nColumn::Int64, - columns::Vector{Int64}, - nCols::Int64, - smoothness::Int64, - extrapolation::Int64, + nRow::Integer, + nColumn::Integer, + columns::AbstractVector{<:Integer}, + nCols::Integer, + smoothness::Integer, + extrapolation::Integer, verbose::Integer) #= Converts the table into the C format, that is double* =# local tableCShape = reduce(vcat, [table[j,i] for i in 1:size(table,2), j in 1:size(table,1)]) + local columnsCInt = convert(Vector{Cint}, columns) local res = ccall((:ModelicaStandardTables_CombiTable1D_init2, installedLibPath), Ptr{Cvoid}, (Cstring, Cstring, Ptr{Cdouble}, Csize_t, Csize_t, Ptr{Cint}, Csize_t, Cint, Cint, Cint), - fileName, tableName, tableCShape, nRow, nColumn, columns, nCols, smoothness, extrapolation, verbose) + fileName, tableName, tableCShape, nRow, nColumn, columnsCInt, nCols, smoothness, extrapolation, verbose) res end @@ -431,21 +432,22 @@ function ModelicaStandardTables_CombiTimeTable_init2( fileName::String, tableName::String, table::Matrix{Float64}, - nRow::Int64, - nColumn::Int64, + nRow::Integer, + nColumn::Integer, startTime::Float64, - columns::Vector{Int64}, - nCols::Int64, - smoothness::Int64, - extrapolation::Int64, + columns::AbstractVector{<:Integer}, + nCols::Integer, + smoothness::Integer, + extrapolation::Integer, shiftTime::Float64, - timeEvents::Int64, + timeEvents::Integer, verbose::Integer) #= Converts the table into the C format, that is double* =# local tableCShape = reduce(vcat, [table[j,i] for i in 1:size(table,2), j in 1:size(table,1)]) + local columnsCInt = convert(Vector{Cint}, columns) local res = ccall((:ModelicaStandardTables_CombiTimeTable_init2, installedLibPath), Ptr{Cvoid}, (Cstring, Cstring, Ptr{Cdouble}, Csize_t, Csize_t, Cdouble, Ptr{Cint}, Csize_t, Cint, Cint, Cdouble, Cint, Cint), - fileName, tableName, tableCShape, nRow, nColumn, startTime, columns, nCols, smoothness, extrapolation, shiftTime, timeEvents, verbose) + fileName, tableName, tableCShape, nRow, nColumn, startTime, columnsCInt, nCols, smoothness, extrapolation, shiftTime, timeEvents, verbose) res end @@ -665,10 +667,10 @@ function ModelicaStandardTables_CombiTable2D_init2( fileName::String, tableName::String, table::Matrix{Float64}, - nRow::Int64, - nColumn::Int64, - smoothness::Int64, - extrapolation::Int64, + nRow::Integer, + nColumn::Integer, + smoothness::Integer, + extrapolation::Integer, verbose::Integer) #= Converts the table into the C format, that is double* =# local tableCShape = reduce(vcat, [table[j,i] for i in 1:size(table,2), j in 1:size(table,1)]) @@ -997,6 +999,17 @@ function ModelicaIO_readMatrixSizes(fileName::String, matrixName::String)::Vecto return Int64[dim[1], dim[2]] end +#= Output-by-reference overload matching the Modelica `external "C"` decl + `ModelicaIO_readMatrixSizes(fileName, matrixName, dim)`. =# +function ModelicaIO_readMatrixSizes(fileName::AbstractString, + matrixName::AbstractString, + dim::AbstractVector) + local res = ModelicaIO_readMatrixSizes(String(fileName), String(matrixName)) + dim[1] = res[1] + dim[2] = res[2] + return dim +end + """ ModelicaIO_readRealMatrix(fileName, matrixName, nrow, ncol, verbose) -> Matrix{Float64} @@ -1021,6 +1034,26 @@ function ModelicaIO_readRealMatrix( return Matrix{Float64}(reshape(buffer, ncol, nrow)') end +#= Output-by-reference overload matching the Modelica `external "C"` decl + `ModelicaIO_readRealMatrix(fileName, matrixName, matrix, size(matrix,1), size(matrix,2), verbose)`. + `matrix` may arrive as a `Matrix{Cdouble}(nrow,ncol)` (constant dims folded) or as a + degenerate `Cdouble[]` (runtime-sized) — both shapes accept linear `copyto!`. =# +function ModelicaIO_readRealMatrix(fileName::AbstractString, + matrixName::AbstractString, + matrix::AbstractArray, + nrow::Integer, + ncol::Integer, + verbose::Bool = true) + local res = ModelicaIO_readRealMatrix(String(fileName), String(matrixName), + Int64(nrow), Int64(ncol), verbose) + local need = Int(nrow) * Int(ncol) + if matrix isa Vector && length(matrix) != need + resize!(matrix, need) + end + copyto!(matrix, res) + return matrix +end + """ ModelicaIO_writeRealMatrix(fileName, matrixName, matrix; append, version) @@ -1047,6 +1080,16 @@ function ModelicaIO_writeRealMatrix( return Int64(rc) end +#= Output-by-reference style overload matching the Modelica `external "C"` decl + `success = ModelicaIO_writeRealMatrix(fileName, matrixName, matrix, size(matrix,1), size(matrix,2), append, format)`. + The redundant nrow/ncol args are derived from `matrix` itself; we accept and ignore them. =# +function ModelicaIO_writeRealMatrix(fileName::AbstractString, matrixName::AbstractString, + matrix::AbstractMatrix, nrow::Integer, ncol::Integer, + append::Bool, version::AbstractString)::Int64 + return ModelicaIO_writeRealMatrix(String(fileName), String(matrixName), + Matrix{Float64}(matrix), Bool(append), String(version)) +end + #= ---- ModelicaInternal functions (via safe_* wrappers in libModelicaCallbacks) ---- =# """ @@ -1084,6 +1127,21 @@ function ModelicaInternal_readLine(fileName::String, lineNumber::Int64) return (str, endOfFile[] != 0) end +#= Output-by-reference overload matching the Modelica `external "C"` decl + `line = ModelicaInternal_readLine(fileName, lineNumber, endOfFile)`. The + third arg is a Ref/Vector slot the C function writes the EOF flag into; we + delegate to the 2-arg form and store the result. =# +function ModelicaInternal_readLine(fileName::AbstractString, lineNumber::Integer, + endOfFile)::String + local (line, isEnd) = ModelicaInternal_readLine(String(fileName), Int64(lineNumber)) + if endOfFile isa Ref + endOfFile[] = isEnd ? Cint(1) : Cint(0) + elseif endOfFile isa AbstractArray && !isempty(endOfFile) + endOfFile[1] = isEnd ? Cint(1) : Cint(0) + end + return line +end + """ ModelicaInternal_countLines(fileName) -> Int64 diff --git a/test/runtests.jl b/test/runtests.jl index e8e0d81..4f028ef 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -366,6 +366,43 @@ const ORC = OMRuntimeExternalC rm(f, force=true) end + @testset "readMatrixSizes output-by-reference overload" begin + local dim = zeros(Cint, 2) + local ret = ORC.ModelicaIO_readMatrixSizes(testMatFile, "testMatrix", dim) + @test dim == Cint[2, 3] + @test ret === dim + end + + @testset "readRealMatrix output-by-reference overload (Matrix buffer)" begin + local buf = zeros(Cdouble, 2, 3) + local ret = ORC.ModelicaIO_readRealMatrix(testMatFile, "testMatrix", buf, 2, 3) + @test ret === buf + @test isapprox(buf[1, 1], 1.0, atol=1e-10) + @test isapprox(buf[1, 3], 3.0, atol=1e-10) + @test isapprox(buf[2, 2], 5.0, atol=1e-10) + @test isapprox(buf[2, 3], 6.0, atol=1e-10) + end + + @testset "readRealMatrix output-by-reference overload (Vector fallback)" begin + local buf = Cdouble[] + local ret = ORC.ModelicaIO_readRealMatrix(testMatFile, "testMatrix", buf, 2, 3) + @test ret === buf + @test length(buf) == 6 + @test isapprox(buf[1], 1.0, atol=1e-10) + @test isapprox(buf[6], 6.0, atol=1e-10) + end + + @testset "writeRealMatrix Modelica external arg-shape overload" begin + local f = tempname() * ".mat" + local A = [11.0 12.0; 21.0 22.0; 31.0 32.0] + local rc = ORC.ModelicaIO_writeRealMatrix(f, "Matrix_A", A, 3, 2, false, "4") + @test rc == 1 + @test isfile(f) + local dims = ORC.ModelicaIO_readMatrixSizes(f, "Matrix_A") + @test dims == [3, 2] + rm(f, force=true) + end + rm(testMatFile, force=true) end From 5b7b1871b5f132dbe26a9d3b4cca4b8bfcbcc876 Mon Sep 17 00:00:00 2001 From: JKRT Date: Thu, 11 Jun 2026 11:04:18 +0200 Subject: [PATCH 2/2] Add AD-safe overloads, Random.Utilities impure RNG, and scanInteger Ref overload Adds ForwardDiff-transparent wrappers for CombiTimeTable getValue/getDerValue/getDer2Value (strip Dual to primal so table lookups are constant w.r.t. differentiated unknowns), a Ref-accepting overload for ModelicaStrings_scanInteger, and Julia implementations of Modelica.Math.Random.Utilities initializeImpureRandom/impureRandom/impureRandomInteger backed by the existing ModelicaRandom_* C primitives. Tests added for all new paths. Co-Authored-By: JKRT <8775827+JKRT@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 --- src/api.jl | 143 +++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 71 +++++++++++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/src/api.jl b/src/api.jl index aa6787d..fc78583 100644 --- a/src/api.jl +++ b/src/api.jl @@ -491,6 +491,30 @@ function ModelicaStandardTables_CombiTimeTable_getValue( tableID, icol, t, nextTimeEvent, preNextTimeEvent) end +#= Strip a possibly-AD (ForwardDiff.Dual) Real to its primal Cdouble WITHOUT a + ForwardDiff dependency: a Dual stores its primal in the `value` field, so peel + it recursively until an AbstractFloat (handles nested/higher-order Duals). =# +@inline _orcPrimal(x::AbstractFloat)::Float64 = Float64(x) +@inline _orcPrimal(x::Integer)::Float64 = Float64(x) +@inline _orcPrimal(x::Real)::Float64 = _orcPrimal(getfield(x, :value)) + +#= AD-safe overload: the DAE consistent-IC nonlinear solve autodiffs the RHS, so + the table getter can be called with Dual args on the event-timing parameters. + The table value depends only on `t` (constant during that solve), so it is + constant w.r.t. the differentiated unknowns (zero derivative). Strip to primals + and return the numeric value; the Float64 return is treated as a constant by + ForwardDiff, which is the correct zero-derivative through the table. =# +function ModelicaStandardTables_CombiTimeTable_getValue( + tableID::ExternalCombiTimeTable, + icol::Integer, + t::Real, + nextTimeEvent::Real, + preNextTimeEvent::Real, + )::Float64 + ModelicaStandardTables_CombiTimeTable_getValue(tableID, Int64(icol), + _orcPrimal(t), _orcPrimal(nextTimeEvent), _orcPrimal(preNextTimeEvent)) +end + """ MODELICA_EXPORT double ModelicaStandardTables_CombiTimeTable_getDerValue(void* tableID, int icol, double t, double nextTimeEvent, double preNextTimeEvent, double der_t); @@ -525,6 +549,20 @@ function ModelicaStandardTables_CombiTimeTable_getDerValue( return res end +#= AD-safe overload (see CombiTimeTable_getValue): a time-function, constant + w.r.t. the differentiated unknowns; strip any Dual args to primals. =# +function ModelicaStandardTables_CombiTimeTable_getDerValue( + tableID::ExternalCombiTimeTable, + icol::Integer, + t::Real, + nextTimeEvent::Real, + preNextTimeEvent::Real, + der_t::Real, + ) + ModelicaStandardTables_CombiTimeTable_getDerValue(tableID, Int64(icol), + _orcPrimal(t), _orcPrimal(nextTimeEvent), _orcPrimal(preNextTimeEvent), _orcPrimal(der_t)) +end + """ MODELICA_EXPORT double ModelicaStandardTables_CombiTimeTable_getDer2Value(void* tableID, int icol, double t, double nextTimeEvent, double preNextTimeEvent, @@ -562,6 +600,21 @@ function ModelicaStandardTables_CombiTimeTable_getDer2Value( return res end +#= AD-safe overload (see CombiTimeTable_getValue): a time-function, constant + w.r.t. the differentiated unknowns; strip any Dual args to primals. =# +function ModelicaStandardTables_CombiTimeTable_getDer2Value( + tableID::ExternalCombiTimeTable, + icol::Integer, + t::Real, + nextTimeEvent::Real, + preNextTimeEvent::Real, + der_t::Real, + der2_t::Real, + ) + ModelicaStandardTables_CombiTimeTable_getDer2Value(tableID, Int64(icol), + _orcPrimal(t), _orcPrimal(nextTimeEvent), _orcPrimal(preNextTimeEvent), _orcPrimal(der_t), _orcPrimal(der2_t)) +end + """ MODELICA_EXPORT double ModelicaStandardTables_CombiTimeTable_minimumTime(void* tableID); """ @@ -916,6 +969,14 @@ function ModelicaStrings_scanInteger(string::String, startIndex::Int64, unsigned return (Int64(nextIndex[]), Int64(integerNumber[])) end +function ModelicaStrings_scanInteger(string::String, startIndex::Integer, unsignedNumber::Integer, + nextIndex::Ref{Cint}, integerNumber::Ref{Cint}) + ccall((:ModelicaStrings_scanInteger, installedLibPathlibModelicaExternalC), + Cvoid, + (Cstring, Cint, Cint, Ref{Cint}, Ref{Cint}), + string, Cint(startIndex), Cint(unsignedNumber), nextIndex, integerNumber) +end + """ MODELICA_EXPORT void ModelicaStrings_scanReal(_In_z_ const char* string, int startIndex, int unsignedNumber, int* nextIndex, double* number); @@ -1330,6 +1391,88 @@ function ModelicaRandom_impureRandom_xorshift1024star(id::Integer)::Float64 ) end +#= ---- Modelica.Math.Random.Utilities impure-RNG functions ---- + The codegen does not translate these MSL algorithm bodies, so provide them + here (routed via MODELICA_UTILITIES_TO_RUNTIME_C). Faithful to the MSL 3.2.3 + sources, built on the ModelicaRandom_* C primitives above. =# + +#= Generators.Xorshift64star.initialState(localSeed, globalSeed): seed a length-2 + state (a fixed prime replaces an all-zero seed) and iterate 10 times. =# +function _xorshift64starInitialState(localSeed::Integer, globalSeed::Integer) + local state = (localSeed == 0 && globalSeed == 0) ? + Cint[126247697, Cint(globalSeed)] : Cint[Cint(localSeed), Cint(globalSeed)] + local out = Cint[0, 0] + local r = Ref{Cdouble}(0.0) + for _ in 1:10 + ModelicaRandom_xorshift64star(state, out, r) + state[1] = out[1]; state[2] = out[2] + end + return state +end + +#= Utilities.initialStateWithXorshift64star(localSeed, globalSeed, nState): fill an + nState-length Integer state vector with xorshift64* draws (pairwise). =# +function _initialStateWithXorshift64star(localSeed::Integer, globalSeed::Integer, nState::Integer) + local state = zeros(Cint, nState) + local aux = _xorshift64starInitialState(localSeed, globalSeed) + if nState >= 2 + state[1] = aux[1]; state[2] = aux[2] + else + state[1] = aux[1] + end + local nStateEven = 2 * div(nState, 2) + local out = Cint[0, 0] + local r = Ref{Cdouble}(0.0) + local i = 3 + while i <= nStateEven + ModelicaRandom_xorshift64star(Cint[state[i-2], state[i-1]], out, r) + state[i] = out[1]; state[i+1] = out[2] + i += 2 + end + if nState >= 3 && nState != nStateEven + ModelicaRandom_xorshift64star(Cint[state[nState-2], state[nState-1]], out, r) + state[nState] = out[1] + end + return state +end + +""" + Modelica_Math_Random_Utilities_initializeImpureRandom(seed) -> Int + +Modelica.Math.Random.Utilities.initializeImpureRandom: seed the impure +xorshift1024* generator's hidden C state from `seed` and return the id +(the constant localSeed) to be passed to impureRandom. +""" +function Modelica_Math_Random_Utilities_initializeImpureRandom(seed::Integer)::Int + local localSeed = 715827883 + #= MSL passes localSeed as the local seed and `seed` as the global seed. =# + local rngState = _initialStateWithXorshift64star(localSeed, seed, 33) + local id = localSeed + ModelicaRandom_setInternalState_xorshift1024star(rngState, length(rngState), id) + return id +end + +""" + Modelica_Math_Random_Utilities_impureRandom(id) -> Float64 + +Modelica.Math.Random.Utilities.impureRandom: draw the next impure sample. +""" +Modelica_Math_Random_Utilities_impureRandom(id::Integer)::Float64 = + ModelicaRandom_impureRandom_xorshift1024star(id) + +""" + Modelica_Math_Random_Utilities_impureRandomInteger(id, imin, imax) -> Int + +Modelica.Math.Random.Utilities.impureRandomInteger: map an impure sample to the +integer range [imin, imax]. +""" +function Modelica_Math_Random_Utilities_impureRandomInteger(id::Integer, + imin::Integer = 1, + imax::Integer = 268435456)::Int + local r = ModelicaRandom_impureRandom_xorshift1024star(id) + return min(imax, floor(Int, r * (imax - imin + 1)) + imin) +end + """ ModelicaRandom_automaticGlobalSeed(dummy) -> Int diff --git a/test/runtests.jl b/test/runtests.jl index 4f028ef..11ec8e9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -527,6 +527,25 @@ const ORC = OMRuntimeExternalC @test nextIdx2 == 5 end + @testset "ModelicaStrings_scanInteger Ref-accepting overload" begin + local nextIdx = Ref{Cint}(0) + local intVal = Ref{Cint}(0) + ORC.ModelicaStrings_scanInteger("42 hello", Int64(1), Int64(0), nextIdx, intVal) + @test intVal[] == 42 + @test nextIdx[] == 3 + + local nextIdx2 = Ref{Cint}(0) + local intVal2 = Ref{Cint}(0) + ORC.ModelicaStrings_scanInteger("-123abc", Int64(1), Int64(0), nextIdx2, intVal2) + @test intVal2[] == -123 + @test nextIdx2[] == 5 + + local nextIdx3 = Ref{Cint}(0) + local intVal3 = Ref{Cint}(0) + ORC.ModelicaStrings_scanInteger("7", Int64(1), false, nextIdx3, intVal3) + @test intVal3[] == 7 + end + @testset "ModelicaStrings_scanReal" begin (nextIdx, val) = ORC.ModelicaStrings_scanReal("3.14 rest", 1, 0) @test isapprox(val, 3.14, atol=1e-10) @@ -678,6 +697,58 @@ const ORC = OMRuntimeExternalC local y3 = ORC.ModelicaRandom_impureRandom_xorshift1024star(0) @test y3 == y1 end + + @testset "Modelica.Math.Random.Utilities impure RNG (ORC re-impl)" begin + #= initializeImpureRandom seeds the hidden xorshift1024* state and returns + the constant id (localSeed = 715827883). =# + local id = ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(12345) + @test id isa Int + @test id == 715827883 + + @testset "state fill helper is full-length and deterministic" begin + local st1 = ORC._initialStateWithXorshift64star(715827883, 99, 33) + local st2 = ORC._initialStateWithXorshift64star(715827883, 99, 33) + @test length(st1) == 33 + @test eltype(st1) == Cint + @test st1 == st2 + #= a different global seed yields a different state =# + @test st1 != ORC._initialStateWithXorshift64star(715827883, 100, 33) + end + + @testset "impureRandom draws in [0, 1)" begin + local r1 = ORC.Modelica_Math_Random_Utilities_impureRandom(id) + local r2 = ORC.Modelica_Math_Random_Utilities_impureRandom(id) + @test r1 isa Float64 + @test 0.0 <= r1 < 1.0 + @test 0.0 <= r2 < 1.0 + @test r1 != r2 #= impure: successive draws differ =# + end + + @testset "deterministic in the seed (same seed -> same stream)" begin + ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(777) + local a = [ORC.Modelica_Math_Random_Utilities_impureRandom(id) for _ in 1:3] + ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(777) + local b = [ORC.Modelica_Math_Random_Utilities_impureRandom(id) for _ in 1:3] + @test a == b + end + + @testset "different seeds -> different stream" begin + ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(1) + local v1 = [ORC.Modelica_Math_Random_Utilities_impureRandom(id) for _ in 1:3] + ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(2) + local v2 = [ORC.Modelica_Math_Random_Utilities_impureRandom(id) for _ in 1:3] + @test v1 != v2 + end + + @testset "impureRandomInteger maps into [imin, imax]" begin + ORC.Modelica_Math_Random_Utilities_initializeImpureRandom(42) + for _ in 1:50 + local k = ORC.Modelica_Math_Random_Utilities_impureRandomInteger(id, 1, 10) + @test k isa Int + @test 1 <= k <= 10 + end + end + end end end