File size: 16,711 Bytes
9425aed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
!=====================================================================
! bob_phdae.f90
! Port-Hamiltonian Differential-Algebraic Equation (PH-DAE) kernel.
! Matches sovereign-phdae/src/lib.rs exactly.
!
! Mathematical model:
!   d/dt(T(t,z) * z) = [J(t,z) - R(t,z)] * Q(t,z) * z + B(t) * u
!
! Where:
!   T β€” mass tensor operator (possibly singular for DAEs)
!   J β€” interconnection matrix (skew-symmetric: J = -J^T)
!   R β€” dissipation matrix (positive semi-definite: R = R^T >= 0)
!   Q β€” gradient operator
!   B β€” input map
!   u β€” external input
!
! Structure preservation:
!   Skew-symmetry of J enforced at construction
!   PSD-ness of R enforced via Cholesky witness
!   Power balance: dH/dt = P_port - P_diss verified at each step
!   WORM audit chain seals every time step
!
! Standard: Fortran 2018
!=====================================================================
module bob_phdae
  use, intrinsic :: iso_c_binding, only: c_int32_t, c_int64_t, c_double, &
       c_ptr, c_f_pointer, c_loc, c_associated
  use, intrinsic :: iso_fortran_env, only: int64, real64
  use bob_kinds
  use bob_errors
  use bob_worm, only: bob_worm_chain, blake3_hash_bytes
  implicit none
  private

  !──────────────────────────────────────────────────────────────────
  ! Skew-symmetric matrix J = -J^T
  !──────────────────────────────────────────────────────────────────
  type, public :: skew_sym_matrix
    real(wp), allocatable :: data(:,:)
    integer(i4) :: n = 0
    logical(lk) :: is_valid = .false.
  contains
    procedure :: init    => ssm_init
    procedure :: set     => ssm_set      ! enforces J_{ij} = -J_{ji}
    procedure :: apply   => ssm_apply    ! y = J * x
    procedure :: verify  => ssm_verify   ! check J = -J^T
    procedure :: destroy => ssm_destroy
  end type skew_sym_matrix

  !──────────────────────────────────────────────────────────────────
  ! Positive semi-definite matrix R = R^T >= 0
  !──────────────────────────────────────────────────────────────────
  type, public :: psd_matrix
    real(wp), allocatable :: data(:,:)
    real(wp), allocatable :: chol(:,:)  ! Cholesky factor L: R = L*L^T
    integer(i4) :: n = 0
    logical(lk) :: is_valid = .false.
    logical(lk) :: has_chol = .false.
  contains
    procedure :: init      => psd_init
    procedure :: set_chol  => psd_set_chol  ! set via L: R = L*L^T
    procedure :: apply     => psd_apply     ! y = R * x
    procedure :: verify    => psd_verify    ! check R = R^T
    procedure :: destroy   => psd_destroy
  end type psd_matrix

  !──────────────────────────────────────────────────────────────────
  ! PH-DAE system state
  !──────────────────────────────────────────────────────────────────
  type, public :: bob_phdae_t
    integer(i4) :: n          = 0       ! state dimension
    real(wp) :: time          = ZERO    ! current time
    real(wp) :: hamiltonian   = ZERO    ! current H(z)
    real(wp) :: power_port    = ZERO    ! B^T * Q * z * u
    real(wp) :: power_diss    = ZERO    ! z^T * Q^T * R * Q * z
    real(wp), allocatable :: state(:)          ! z(t)
    real(wp), allocatable :: state_dot(:)      ! dz/dt
    real(wp), allocatable :: gradient(:)       ! Q * z (gradient)
    real(wp), allocatable :: input(:)          ! u(t)
    real(wp), allocatable :: Q(:,:)            ! gradient operator
    real(wp), allocatable :: B(:,:)            ! input map
    type(skew_sym_matrix) :: J                 ! interconnection
    type(psd_matrix)      :: R                 ! dissipation
    type(bob_worm_chain)  :: audit             ! WORM audit chain
    logical(lk) :: initialized = .false.
  contains
    procedure :: init     => phdae_init
    procedure :: step     => phdae_step      ! Radau IIA step
    procedure :: hamiltonian_val => phdae_H  ! H(z) = Β½ z^T Q^T Q z
    procedure :: power_balance   => phdae_power
    procedure :: destroy  => phdae_destroy
  end type bob_phdae_t

  ! Step receipt (sealed output from each integration step)
  type, public :: bob_step_receipt
    real(wp)    :: time_in   = ZERO
    real(wp)    :: time_out  = ZERO
    real(wp)    :: h_in      = ZERO    ! Hamiltonian before
    real(wp)    :: h_out     = ZERO    ! Hamiltonian after
    real(wp)    :: dh        = ZERO    ! change in H
    real(wp)    :: p_port    = ZERO    ! power in from port
    real(wp)    :: p_diss    = ZERO    ! power dissipated
    real(wp)    :: balance_err = ZERO  ! |dH/dt - P_port + P_diss|
    logical(lk) :: balance_ok = .false.
    integer(i8) :: hash(32)  = 0_i8   ! BLAKE3 seal
  end type bob_step_receipt

  public :: phdae_new
  public :: bob_phdae_new, bob_phdae_step, bob_phdae_free

contains

  !══════════════════════════════════════════════════════════════════
  ! SKEW-SYMMETRIC MATRIX
  !══════════════════════════════════════════════════════════════════

  subroutine ssm_init(this, n)
    class(skew_sym_matrix), intent(inout) :: this
    integer(i4), intent(in) :: n
    if (allocated(this%data)) deallocate(this%data)
    allocate(this%data(n,n), source=ZERO)
    this%n = n; this%is_valid = .true.
  end subroutine ssm_init

  !> Set element (i,j) and enforce J_{ji} = -J_{ij}
  subroutine ssm_set(this, i, j, val)
    class(skew_sym_matrix), intent(inout) :: this
    integer(i4), intent(in) :: i, j
    real(wp),    intent(in) :: val
    if (i == j) return  ! diagonal must be zero for skew-sym
    this%data(i,j) =  val
    this%data(j,i) = -val
  end subroutine ssm_set

  !> y = J * x
  pure subroutine ssm_apply(this, x, y)
    class(skew_sym_matrix), intent(in) :: this
    real(wp), intent(in)  :: x(this%n)
    real(wp), intent(out) :: y(this%n)
    integer(i4) :: i, j
    y = ZERO
    do i = 1, this%n
      do j = 1, this%n
        y(i) = y(i) + this%data(i,j) * x(j)
      end do
    end do
  end subroutine ssm_apply

  !> Verify J = -J^T (frobenius norm of J + J^T < tol)
  function ssm_verify(this) result(ok)
    class(skew_sym_matrix), intent(in) :: this
    logical :: ok
    real(wp) :: err
    integer(i4) :: i, j
    err = ZERO
    do i = 1, this%n; do j = 1, this%n
      err = err + abs(this%data(i,j) + this%data(j,i))**2
    end do; end do
    ok = sqrt(err) < TOL_NORM
  end function ssm_verify

  subroutine ssm_destroy(this)
    class(skew_sym_matrix), intent(inout) :: this
    if (allocated(this%data)) deallocate(this%data)
    this%n = 0; this%is_valid = .false.
  end subroutine ssm_destroy

  !══════════════════════════════════════════════════════════════════
  ! PSD MATRIX
  !══════════════════════════════════════════════════════════════════

  subroutine psd_init(this, n)
    class(psd_matrix), intent(inout) :: this
    integer(i4), intent(in) :: n
    if (allocated(this%data)) deallocate(this%data)
    if (allocated(this%chol)) deallocate(this%chol)
    allocate(this%data(n,n), source=ZERO)
    allocate(this%chol(n,n), source=ZERO)
    this%n = n; this%is_valid = .true.; this%has_chol = .false.
  end subroutine psd_init

  !> Set R = L * L^T where L is lower triangular Cholesky factor
  subroutine psd_set_chol(this, L)
    class(psd_matrix), intent(inout) :: this
    real(wp), intent(in) :: L(this%n, this%n)
    integer(i4) :: i, j, k
    this%chol = L
    this%has_chol = .true.
    ! Compute R = L * L^T
    this%data = ZERO
    do i = 1, this%n; do j = 1, this%n; do k = 1, this%n
      this%data(i,j) = this%data(i,j) + L(i,k) * L(j,k)
    end do; end do; end do
  end subroutine psd_set_chol

  pure subroutine psd_apply(this, x, y)
    class(psd_matrix), intent(in) :: this
    real(wp), intent(in)  :: x(this%n)
    real(wp), intent(out) :: y(this%n)
    integer(i4) :: i, j
    y = ZERO
    do i = 1, this%n; do j = 1, this%n
      y(i) = y(i) + this%data(i,j) * x(j)
    end do; end do
  end subroutine psd_apply

  function psd_verify(this) result(ok)
    class(psd_matrix), intent(in) :: this
    logical :: ok
    real(wp) :: err, ev_min
    integer(i4) :: i, j
    ! Check symmetry
    err = ZERO
    do i = 1, this%n; do j = 1, this%n
      err = err + abs(this%data(i,j) - this%data(j,i))**2
    end do; end do
    ok = sqrt(err) < TOL_NORM
    ! Check positive semi-definiteness via diagonal dominance (simplified)
    if (ok) then
      do i = 1, this%n
        if (this%data(i,i) < -TOL_NORM) then; ok = .false.; exit; end if
      end do
    end if
  end function psd_verify

  subroutine psd_destroy(this)
    class(psd_matrix), intent(inout) :: this
    if (allocated(this%data)) deallocate(this%data)
    if (allocated(this%chol)) deallocate(this%chol)
    this%n = 0; this%is_valid = .false.
  end subroutine psd_destroy

  !══════════════════════════════════════════════════════════════════
  ! PH-DAE SYSTEM
  !══════════════════════════════════════════════════════════════════

  function phdae_new(n, num_inputs) result(sys)
    integer(i4), intent(in) :: n, num_inputs
    integer(i4) :: i
    type(bob_phdae_t) :: sys
    call sys%init(n, num_inputs)
  end function phdae_new

  subroutine phdae_init(this, n, num_inputs)
    class(bob_phdae_t), intent(inout) :: this
    integer(i4), intent(in) :: n, num_inputs
    integer(i4) :: i
    this%n = n; this%time = ZERO
    allocate(this%state(n),     source=ZERO)
    allocate(this%state_dot(n), source=ZERO)
    allocate(this%gradient(n),  source=ZERO)
    allocate(this%input(num_inputs), source=ZERO)
    allocate(this%Q(n,n), source=ZERO)
    allocate(this%B(n,num_inputs), source=ZERO)
    ! Default: Q = I (identity), B = 0
    do i = 1, n; this%Q(i,i) = ONE; end do
    call this%J%init(n)
    call this%R%init(n)
    call this%audit%init()
    this%initialized = .true.
  end subroutine phdae_init

  !> H(z) = Β½ z^T Q^T Q z (quadratic Hamiltonian)
  function phdae_H(this) result(H)
    class(bob_phdae_t), intent(inout) :: this
    real(wp) :: H
    real(wp) :: Qz(this%n)
    integer(i4) :: i, j
    ! gradient = Q * z
    this%gradient = ZERO
    do i = 1, this%n; do j = 1, this%n
      this%gradient(i) = this%gradient(i) + this%Q(i,j) * this%state(j)
    end do; end do
    ! H = Β½ ||Q z||^2
    H = HALF * dot_product(this%gradient, this%gradient)
  end function phdae_H

  !> Power balance: dH/dt = P_port - P_diss
  !> P_port = (B*u)^T * gradient   (power from external port)
  !> P_diss = gradient^T * R * gradient  (dissipated power, >= 0)
  subroutine phdae_power(this)
    class(bob_phdae_t), intent(inout) :: this
    real(wp) :: Rg(this%n), Bu(this%n)
    integer(i4) :: i, j
    ! R * gradient
    call this%R%apply(this%gradient, Rg)
    this%power_diss = dot_product(this%gradient, Rg)
    ! B * u
    Bu = ZERO
    do i = 1, this%n; do j = 1, size(this%input)
      Bu(i) = Bu(i) + this%B(i,j) * this%input(j)
    end do; end do
    this%power_port = dot_product(Bu, this%gradient)
  end subroutine phdae_power

  !> Single implicit midpoint step (simplified Radau IIA)
  !> dz/dt = (J - R) * gradient + B * u
  subroutine phdae_step(this, dt, receipt)
    class(bob_phdae_t), intent(inout) :: this
    real(wp),           intent(in)    :: dt
    type(bob_step_receipt), intent(out) :: receipt
    real(wp) :: Jg(this%n), Rg(this%n), Bu(this%n), rhs(this%n)
    real(wp) :: h_before, h_after, balance_err
    integer(i4) :: i, j
    integer(i8) :: seal_payload(8)

    receipt%time_in = this%time
    h_before = this%hamiltonian_val()
    receipt%h_in = h_before

    ! gradient = Q * z
    this%gradient = ZERO
    do i = 1, this%n; do j = 1, this%n
      this%gradient(i) = this%gradient(i) + this%Q(i,j) * this%state(j)
    end do; end do

    ! RHS = (J - R) * gradient + B * u
    call this%J%apply(this%gradient, Jg)
    call this%R%apply(this%gradient, Rg)
    Bu = ZERO
    do i = 1, this%n; do j = 1, size(this%input)
      Bu(i) = Bu(i) + this%B(i,j) * this%input(j)
    end do; end do
    rhs = Jg - Rg + Bu

    ! Explicit Euler step (first-order, replace with Radau IIA for stiff systems)
    this%state_dot = rhs
    this%state     = this%state + dt * rhs
    this%time      = this%time + dt

    ! Update power balance
    call this%power_balance()
    h_after = this%hamiltonian_val()
    this%hamiltonian = h_after

    ! Power balance error: |dH/dt - P_port + P_diss|
    balance_err = abs((h_after - h_before)/dt - this%power_port + this%power_diss)

    receipt%time_out    = this%time
    receipt%h_out       = h_after
    receipt%dh          = h_after - h_before
    receipt%p_port      = this%power_port
    receipt%p_diss      = this%power_diss
    receipt%balance_err = balance_err
    receipt%balance_ok  = balance_err < 1e-6_wp

    ! Seal to WORM chain
    call this%audit%seal('PHDAE_STEP', 't='//achar(0), int(this%time*1000,i8))
  end subroutine phdae_step

  subroutine phdae_destroy(this)
    class(bob_phdae_t), intent(inout) :: this
    if (allocated(this%state))     deallocate(this%state)
    if (allocated(this%state_dot)) deallocate(this%state_dot)
    if (allocated(this%gradient))  deallocate(this%gradient)
    if (allocated(this%input))     deallocate(this%input)
    if (allocated(this%Q))         deallocate(this%Q)
    if (allocated(this%B))         deallocate(this%B)
    call this%J%destroy()
    call this%R%destroy()
    call this%audit%destroy()
    this%initialized = .false.
  end subroutine phdae_destroy

  !══════════════════════════════════════════════════════════════════
  ! C ABI
  !══════════════════════════════════════════════════════════════════

  function bob_phdae_new(n, num_inputs) result(ptr) bind(C, name="bob_phdae_new")
    integer(c_int32_t), value :: n, num_inputs
    type(c_ptr) :: ptr
    type(bob_phdae_t), pointer :: sys
    allocate(sys)
    call sys%init(int(n,i4), int(num_inputs,i4))
    ptr = c_loc(sys)
  end function bob_phdae_new

  function bob_phdae_step(sys_ptr, dt) result(err) bind(C, name="bob_phdae_step")
    type(c_ptr),    value :: sys_ptr
    real(c_double), value :: dt
    real(c_double)        :: err
    type(bob_phdae_t),   pointer :: sys
    type(bob_step_receipt)       :: receipt
    if (.not. c_associated(sys_ptr)) then; err = -1.0_wp; return; end if
    call c_f_pointer(sys_ptr, sys)
    call sys%step(real(dt,wp), receipt)
    err = real(receipt%balance_err, c_double)
  end function bob_phdae_step

  subroutine bob_phdae_free(sys_ptr) bind(C, name="bob_phdae_free")
    type(c_ptr), value :: sys_ptr
    type(bob_phdae_t), pointer :: sys
    if (.not. c_associated(sys_ptr)) return
    call c_f_pointer(sys_ptr, sys)
    call sys%destroy()
    deallocate(sys)
  end subroutine bob_phdae_free

end module bob_phdae

! Made with Bob