-
Notifications
You must be signed in to change notification settings - Fork 1
/
ComputeIdentifiableFunctions.mpl
525 lines (453 loc) · 22 KB
/
ComputeIdentifiableFunctions.mpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
###############################################################################
# Part 1: Algorithms for computation with subfields of rational functions
###############################################################################
with(PolynomialIdeals):
#------------------------------------------------------------------------------
IdealsEq := proc(A, B)
# Checks whether polynomial ideals are equal
return evalb((A subset B) and (B subset A)):
end:
#------------------------------------------------------------------------------
FieldToIdeal := proc(gens)
# Input: generators of a subfield of the field of rational functions
# Computes the MSQ ideal of the field with the new variables of the form x_aux
# See: https://mediatum.ub.tum.de/doc/685465/document.pdf Definition 2.16
# https://doi.org/10.1016/j.jsc.2005.09.010 Lemma 2.1
local all_vars, subs_dupl, all_dupl, common_denom, polys, f, gb:
all_vars := indets(gens):
subs_dupl := map(v -> v = cat(v, _aux), all_vars):
all_dupl := sort([op(map(v -> subs(subs_dupl, v), all_vars))]):
common_denom := 1:
polys := []:
for f in gens do
common_denom := lcm(common_denom, denom(f)):
polys := [op(polys), numer(f) * subs(subs_dupl, denom(f)) - subs(subs_dupl, numer(f)) * denom(f)]:
end do:
gb := Groebner[Basis]([op(polys), subs(subs_dupl, common_denom) * t - 1], tdeg(t, op(all_dupl))):
gb := Groebner[Walk](gb, tdeg(t, op(all_dupl)), lexdeg([t], [op(all_dupl)])):
gb := select(p -> not (t in indets(p)), gb):
return PolynomialIdeal(gb, variables=all_dupl):
end proc:
#------------------------------------------------------------------------------
FieldCoefficientRestriction := proc(J, msq_for_subfield)
# Input: J - a polynomial ideals over a field of rational functions
# msq_for_subfield - the MSQ ideal for a subfield E of coefficients (see FieldToIdeal)
# Computes the radical of the restriction of the ideal to the subfield E
# in the sense of https://doi.org/10.1016/j.jsc.2005.09.010 (MSQ-paper in what follows)
#
# NOTE: unlike the algorithm in the MSQ-paper, we compute the radical, not the restriction itself
# one can obtain the algorithm MSQ-paper by replacing PrimeDecomposition with PrimaryDecomposition
# in the code below
local poly_vars, coeff_vars, subs_aux, coeff_aux, gens, subs_aux_msq, gens_msq, msq_ideal_aux,
msq_components, J_ext, components, primes_to_keep, P, elim_P, comp, cleaned_ideal:
poly_vars := IdealInfo[Variables](J):
coeff_vars := IdealInfo[Parameters](J) union IdealInfo[Parameters](msq_for_subfield):
subs_aux := map(v -> v = parse(cat(v, _aux_aux)), coeff_vars):
coeff_aux := subs(subs_aux, coeff_vars):
# list F of polynomials in the notation of the MSQ-paper, page 375
gens := map(p -> subs(subs_aux, p * denom(p)), IdealInfo[Generators](J)):
# Substitution to avoid names clashing between the aux variables in the msq ideal and the variables in J
subs_aux_msq := map(v -> v = parse(cat(v, _aux)), IdealInfo[Variables](msq_for_subfield)):
gens_msq := map(p -> subs(subs_aux_msq, p), IdealInfo[Generators](msq_for_subfield)):
msq_ideal_aux := PolynomialIdeal(gens_msq, variables=map(s -> rhs(s), subs_aux_msq)):
msq_components := [PrimeDecomposition(msq_ideal_aux)]:
J_ext := PolynomialIdeal([op(gens), op(gens_msq)], variables=poly_vars union coeff_aux):
components := [PrimeDecomposition(J_ext)]:
# Selecting prime components as in Remark on page 377 in MSQ-paper
primes_to_keep := []:
for P in components do
# Going through the prime decomposition of the MSQ-deal mimics the
# working over the restricted field (which is what has been done in the MSQ-paper)
elim_P := EliminationIdeal(P, coeff_aux):
for comp in msq_components do
if elim_P subset comp then
primes_to_keep := [op(primes_to_keep), P]:
end if:
end do:
end do:
if nops(primes_to_keep) > 0 then
cleaned_ideal := Intersect(op(primes_to_keep)):
else
cleaned_ideal := PolynomialIdeal([0], variables = poly_vars):
end if:
# Applying Lemma 2.2 from the MSQ-paper
return EliminationIdeal(cleaned_ideal, poly_vars):
end proc:
#------------------------------------------------------------------------------
FilterGenerators := proc(ideal)
# Input: ideal over a rational function field
# Computes a simplified set of generators of the field of definition
local gb, gens, p, cf, lc, gsorted, result, big_ideal, cur_ideal, g, new_ideal:
gb := Groebner[Basis](ideal, tdeg(op(IdealInfo[Variables](ideal)))):
gens := {}:
for p in gb do
cf := sort([coeffs(p, IdealInfo[Variables](ideal))], (a, b) -> length(convert(a, string)) < length(convert(b, string))):
lc := cf[1]:
cf := map(c -> c / lc, cf):
gens := {op(gens), op(cf[2..nops(cf)])}:
end do:
gsorted := sort([op(gens)], (a, b) -> length(convert(a, string)) < length(convert(b, string)));
result := {}:
big_ideal := FieldToIdeal(gens):
cur_ideal := FieldToIdeal(result):
for g in gsorted do
if big_ideal = cur_ideal then
return result:
end if:
new_ideal := FieldToIdeal({op(result), g}):
if new_ideal <> cur_ideal then
# a dirty hack to transform -1/a to a
if convert(g, string)[1] = "-" then
g := -g:
end:
if convert(g, string)[1] = "1" then
g := 1 / g:
end:
result := {op(result), g}:
cur_ideal := new_ideal:
end if:
end do:
return result:
end proc:
#------------------------------------------------------------------------------
FieldIntersection := proc(gens_left, gens_right)
# Input: gens_left and gens_right - generators of a subfields E and F of a field of rational functions
# If terminates, resturns an ideal with field of definition being the intersection of generators of E and F
# Is guaranteed to terminate if at least one of E and F is algebraically closed in rational functions (see REF)
# Implementation below is a version of Algorithm 2.38 from https://mediatum.ub.tum.de/doc/685465/document.pdf
local msq_left, msq_right, poly_vars, coeff_vars, Ii, Ji, gb, result, p, cf, lc;
msq_left := FieldToIdeal(gens_left):
msq_right := FieldToIdeal(gens_right):
poly_vars := IdealInfo[Variables](msq_left) union IdealInfo[Variables](msq_right):
coeff_vars := IdealInfo[Parameters](msq_left) union IdealInfo[Parameters](msq_right):
Ii := PolynomialIdeal([1], variables=poly_vars):
Ji := msq_left:
while not IdealsEq(Ii, Ji) do
Ii := FieldCoefficientRestriction(Ji, msq_right):
Ji := FieldCoefficientRestriction(Ii, msq_left):
end do:
return Ji:
end proc:
#------------------------------------------------------------------------------
CompareFields := proc(gens_l, gens_r)
# Checks whether gens_l and gens_r generate the same subfield of rational functions
return IdealsEq(FieldToIdeal(gens_l), FieldToIdeal(gens_r)):
end proc:
#------------------------------------------------------------------------------
###############################################################################
# Part 2: Algorithm for computing identifiable functions
###############################################################################
with(DifferentialAlgebra):
#------------------------------------------------------------------------------
ExtractDenominator := proc(model)
# Input: model a list of rational functions
# returns the function multiplied by their denominators and
# an inequality corresponding to the LCM of the denominators
local common_denom, r;
common_denom := 1:
for r in model do
common_denom := lcm(common_denom, denom(r)):
end do:
return [op(map(p -> denom(p) * p, model)), common_denom <> 0]:
end proc:
#------------------------------------------------------------------------------
CheckReducibilitySet := proc(polys, charset)
# Input: polys - list of differential polynomials
# charset - a characteristic set
# Returns true iff all the polynomials are reduced to zero wrt to the charset
local result, e:
result := true:
for e in polys do
if NormalForm(e, charset) <> 0 then
result := false:
break:
end if:
end do:
return result:
end proc:
#------------------------------------------------------------------------------
GetIOEquations := proc(model, states, inputs, outputs, params, infolevel)
# Input: model - list of differential polynomials defining the model
# states, inputs, outputs, params - list of names of state, input, output variables
# and parameters, respectively
# Computes a list of input-output equations of the model
local Relim, Rorig, charsets, chset_orig, general_comps, general, c, e, gen_comp, io_eqs:
Relim := DifferentialRing(blocks = [[op(states)], op(outputs), [op(inputs)]], derivations = [t], arbitrary = params):
Rorig := DifferentialRing(blocks = [[op(outputs)], [op(states)], [op(inputs)]], derivations = [t], arbitrary = params):
chset_orig := RosenfeldGroebner(model, Rorig)[1]:
if infolevel > 0 then
printf(" Computing the characteristic set (singsol = none)\n"):
end if:
charsets := RosenfeldGroebner(model, Relim, singsol = none):
if CheckReducibilitySet(Equations(charsets[1]), chset_orig) then
gen_comp := charsets[1]:
else
if infolevel > 0 then
printf(" Did not pick the right component. Using singsol = all\n"):
end if:
charsets := RosenfeldGroebner(model, Relim):
if infolevel > 0 then
printf(" Selecting the general component\n"):
end if:
general_comps := []:
for c in charsets do
general := true:
for e in Equations(c) do
if NormalForm(e, chset_orig) <> 0 then
general := false:
break:
end if:
end do:
if general then
general_comps := [op(general_comps), c]:
end if:
end do:
if nops(general_comps) > 1 then
print("More than one component is considered general!", general_comps):
end if:
gen_comp := general_comps[1]:
end if:
io_eqs := Equations(gen_comp, leader < parse(cat(states[-1], "(t)"))):
return io_eqs:
end proc:
#------------------------------------------------------------------------------
# Adapted from
# https://www.mapleprimes.com/questions/36772-Extract-Specific-Coefficients-Of-A-Multivariate
# by Kitonum 15364
coefff:=proc(P, t)
local L, H, i, k:
L:=[coeffs(P, indets(P), 'h_aux_for_coef')]: H:=[h_aux_for_coef]: k:=0:
for i from 1 to nops(H) do
if H[i]=t then k:=L[i] fi:
end do:
return k;
end proc:
#------------------------------------------------------------------------------
DecomposePolynomial := proc(p, vars_main, vars_coef, infolevel)
# Input: p - polynomial in two groups of variables: vars_main and vars_coef
# Computes a decomposition of minimal length of p as a linear combination
# of products A * B, where A is a polynomial in vars_main and B
# is a polynomial in vars_coef return two lists: list of A's and list of B's
local cf, monoms, result_cf, result_monom, i, c, m, j, lc, lm, coeff_in_c:
cf := [coeffs(collect(p, vars_main, 'distributed'), vars_main, 'monoms')]:
monoms := [monoms]:
result_cf := []:
result_monom := Array([]):
if infolevel > 0 then
printf(" Number of monomials: %a\n", nops(cf)):
end:
for i from 1 to nops(cf) do
c := cf[i]:
m := monoms[i]:
for j from 1 to nops(result_cf) do
lc, lm := Groebner[LeadingTerm](result_cf[j], plex(op(vars_coef))):
coeff_in_c := coefff(c, lm):
c := c - coeff_in_c / lc * result_cf[j]:
result_monom[j] := result_monom[j] + coeff_in_c / lc * m:
end do:
if c <> 0 then
result_cf := [op(result_cf), c]:
ArrayTools[Append](result_monom, m):
end if:
end do:
if infolevel > 0 then
printf(" Reduced to: %a\n", nops(result_cf)):
end:
return [result_cf, convert(result_monom, list)]:
end proc:
#------------------------------------------------------------------------------
ConstructWronskian := proc(io_eq, model, states, inputs, outputs, params, state_space, infolevel)
# Input - the same as for GetIOEquations + one IO-equation + flag subs_param
# Computes the Wronskian for this equation using the representation
# given by DecomposePolynomial. Return a pair of the Wronskian
# reduced modulo the original system and a list of coefficients
# of the compressed io_eq
local diff_to_ord, v, vt, h, v_ord, vd, p, decomp, diff_polys, Rorig, chset_orig,
M, yus, yus_reduced, M_sub, roll, params_sub, ios, ps_sol, ps, i:
diff_to_ord := {}:
ios := [op(inputs), op(outputs)]:
for v in ios do
vt := parse(cat(v, "(t)")):
diff_to_ord := {op(diff_to_ord), vt = v}:
for h from 1 to nops(states) + 1 do
v_ord := parse(cat(v, "_", h)):
vd := diff(vt, t$h):
diff_to_ord := {op(diff_to_ord), vd = v_ord}:
end do:
end do:
p := subs(diff_to_ord, io_eq):
if infolevel > 0 then
printf(" Combining monomials to reduce the dimension\n"):
end if:
decomp := DecomposePolynomial(p, map(e -> rhs(e), diff_to_ord), params, infolevel):
diff_polys := map(p -> subs(map(e -> rhs(e) = lhs(e), diff_to_ord), p), decomp[2]):
Rorig := DifferentialRing(blocks = [[op(outputs)], [op(states)], [op(inputs)]], derivations = [t], arbitrary = params):
chset_orig := RosenfeldGroebner(model, Rorig)[1]:
if infolevel > 0 then
printf(" Computing the Wronskian\n"):
end if:
M := VectorCalculus[Wronskian](diff_polys, t):
yus := indets(M) minus {t}:
if infolevel > 0 then
printf(" Reducing the Wronskian\n"):
end if:
if state_space <> [] then
ps_sol := GetPSSolution(state_space, nops(yus)):
yus_reduced := []:
for v in ios do
vt := parse(cat(v, "(t)")):
ps := subs(ps_sol, v(t)):
for i from 0 to nops(yus) - 1 do
yus_reduced := [op(yus_reduced), vt = subs({t = 0}, ps)]:
vt := diff(vt, t):
ps := diff(ps, t):
end do:
end do:
else
Rorig := DifferentialRing(blocks = [[op(outputs)], [op(states)], [op(inputs)]], derivations = [t], arbitrary = params):
chset_orig := RosenfeldGroebner(model, Rorig)[1]:
yus_reduced := map(p -> p = NormalForm(p, chset_orig), yus):
end if:
M_sub := subs(yus_reduced, M):
M_sub := subs(map(x -> parse(cat(x, "(t)")) = x, states), M_sub):
return [M_sub, decomp[1]]:
end proc:
#------------------------------------------------------------------------------
SingleExperimentIdentifiableFunctions := proc(model, {infolevel := 0})
# Input: model - ODE model in the state-space form
# Computes generators of the field of single-identifiable functions
local states, inputs, outputs, model_eq, ios, params, io_eqs, si_gens, eq, wrnsk, echelon_form, model_denomfree:
states, inputs, outputs, params, model_eq := op(ParseInput(model)):
ios := [op(inputs), op(outputs)]:
# Step 1
if infolevel > 0 then
printf("Step 1: Computing input-output equations\n"):
end if:
model_denomfree := ExtractDenominator(model_eq):
io_eqs := GetIOEquations(model_denomfree, states, inputs, outputs, params, infolevel):
if infolevel > 0 then
printf("Total number of io-equations: %a\n", nops(io_eqs)):
end if:
si_gens := {}:
for eq in io_eqs do
# Step 2
if infolevel > 0 then
printf("Step 2: Constructing the Wronskian\n"):
end if:
wrnsk := ConstructWronskian(eq, model_denomfree, states, inputs, outputs, params, [], infolevel)[1]:
# Step 3
if infolevel > 0 then
printf("Step 3: Computing the reduced row echelon form of the Wronskian\n"):
end if:
echelon_form := LinearAlgebra[ReducedRowEchelonForm](wrnsk):
si_gens := {op(si_gens), op(select(x -> not type(x, numeric), convert(echelon_form, list)))}:
end do:
# Step 4
if infolevel > 0 then
printf("Step 4: restricting to the field of parameters"):
end if:
return FilterGenerators(FieldIntersection(si_gens, params)):
end proc:
#------------------------------------------------------------------------------
# Adapted from https://github.com/pogudingleb/SIAN
FunctionToVariable := proc(f):
convert(convert(f, string)[1..-4], symbol):
end proc:
ParseInput := proc(model)
local all_symbols, x_functions, io_functions, params, states, ios, diff_polys, lhss, out_functions, in_functions, inputs, outputs:
diff_polys := map(eq -> lhs(eq) - rhs(eq), model):
all_symbols := foldl(`union`, op( map(e -> indets(e), diff_polys) )) minus {t}:
x_functions := map(f -> int(f, t), select( f -> type(int(f, t), function(name)), all_symbols )):
io_functions := select( f -> not type(int(f, t), function(name)) and type(f, function(name)) and not f in x_functions, all_symbols ):
lhss := map(eq -> lhs(eq), model):
out_functions := select(f -> f in lhss, io_functions):
in_functions := select(f -> not (f in lhss), io_functions):
params := [op(select(f -> not type(f, function(name)) and not type(int(f, t), function(name)), all_symbols))]:
states := [op(map(FunctionToVariable, x_functions))]:
inputs := [op(map(FunctionToVariable, in_functions))]:
outputs := [op(map(FunctionToVariable, out_functions))]:
return [states, inputs, outputs, params, diff_polys]:
end proc:
#------------------------------------------------------------------------------
MultiExperimentIdentifiableFunctions := proc(model, {infolevel := 0, simplified_generators := false, no_bound := false})
# Input: model - ODE model in the state-space form
# Computes the bound from Theorem REF. Returns a list containing
# 1. the bound
# 2. the list of lists of coefficients of the IO-equations (after compression)
# 3. (optional) simplified set of generators of the field of multi-experiment identifiable functions
#
# Optional keyword arguments:
# 1. simplified_generators - if false, then just the coefficients of the IO-equations are returned.
# if true, they are being simplified (maybe be a substantial simplification)
# but this takes time
# 2. no_bound - if True, then bound for the number of experiments is not computed (may save a lot of time)
local states, inputs, outputs, ios, params, model_eqs, io_eqs, io_coeffs, io_coef, wrnsk, s, roll, wrnsk_sub, r, bound,
generators, i, eq, result, model_denomfree:
states, inputs, outputs, params, model_eqs := op(ParseInput(model)):
if infolevel > 0 then
printf("Computing input-output equations\n"):
end if:
model_denomfree := ExtractDenominator(model_eqs):
io_eqs := GetIOEquations(model_denomfree, states, inputs, outputs, params, infolevel):
if infolevel > 0 then
printf("Total number of io-equations: %a\n", nops(io_eqs)):
end if:
io_coeffs := []:
bound := 0:
for eq in io_eqs do
if no_bound then
io_coef := DecomposePolynomial(eq, indets(eq) minus {op(params)}, params, infolevel)[1]:
else
if infolevel > 0 then
printf("Constructing the Wronskian\n"):
end if:
wrnsk, io_coef := op(ConstructWronskian(eq, model_denomfree, states, inputs, outputs, params, model, infolevel)):
# in the notation of the theorem
s := nops(io_coef) - 1:
r := LinearAlgebra[Rank](wrnsk):
bound := max(bound, s - r + 1)
end if:
io_coeffs := [op(io_coeffs), io_coef]:
end do:
generators := {}:
for io_coef in io_coeffs do
io_coef := sort(io_coef, (a, b) -> length(convert(a, string)) < length(convert(b, string)));
for i from 2 to nops(io_coef) do
generators := {op(generators), io_coef[i] / io_coef[1]}:
end do:
end do:
result := [bound, generators]:
if simplified_generators then
result := [op(result), FilterGenerators(FieldToIdeal(generators))]:
end if:
return result:
end proc:
#------------------------------------------------------------------------------
GetPSSolution := proc(model, ord)
# Input: model and integer ord
# Output: a truncated power series solution with random parameter values
# and initial conditions of order ord
local roll, states, inputs, outputs, params, model_eqs, model_sub,
x_funcs, x_sols, x_eqs, cur_ord, i, rhs_eval, total_sub, y_funcs, y_sols,
params_subs, input_subs:
roll := rand(1..15):
states, inputs, outputs, params, model_eqs := op(ParseInput(model)):
params_subs := map(p -> p = roll(), params):
input_subs := map(u -> parse(cat(u, "(t)")) = add([seq(roll() * t^i, i=0..ord)]), inputs):
model_sub := subs(params_subs, model):
model_sub := subs(input_subs, model_sub):
x_funcs := map(x -> parse(cat(x, "(t)")), states):
x_sols := map(x -> x = roll(), x_funcs):
x_eqs := map(x -> subs(model_sub, diff(x, t)), x_funcs):
for cur_ord from 1 to ord do
for i from 1 to nops(x_funcs) do
rhs_eval := series(subs(x_sols, x_eqs[i]), t, ord + 1):
x_sols[i] := (lhs(x_sols[i]) = (rhs(x_sols[i]) + t^cur_ord * coeff(rhs_eval, t, cur_ord - 1) / cur_ord)):
end do:
end do:
total_sub := [op(x_sols), op(params_subs), op(input_subs)]:
y_funcs := map(y -> parse(cat(y, "(t)")), outputs):
y_sols := map(y -> y = subs(total_sub, subs(model_sub, y)), y_funcs):
return [op(y_sols), op(input_subs), op(params_subs), op(x_sols)]:
end proc:
#------------------------------------------------------------------------------