-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathTensor.lua
More file actions
1300 lines (1126 loc) · 43.5 KB
/
Copy pathTensor.lua
File metadata and controls
1300 lines (1126 loc) · 43.5 KB
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
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--[[
thoughts on transformation between different sets of indexes:
transformIndexes works fine between upper and lower, using the metric and its inverse
I think I have some sort of implicit subset working for my adm calculations between 4D and 3D that just chops off a letter (and neglects the 4D projection part)
... based on the fact that the 3D part of the ADM metric is the 3D spatial metric, and the 3D part of the ADM metric inverse is the 3D spatial metric inverse plus an outer of shift vectors
... which are orthogonal to the spatial metric (so any spatial terms only operate with the 3D portion anyways)
so neglect aside, what is the correct way I should be doing this?
each index set needs a metric (and an inverse, which can be calculated)
...and between each index set needs transform matrices (and their inverses, which can be calculated)
...and between subsequent coordinate systems we can calculate combined transformations
ex:
local t,x,y,z = symmath.vars('t', 'x', 'y', 'z')
local curvedVars = {t,x,y,z}
local tHat, xHat, yHat, zHat = symmath.vars('\hat{t}', '\hat{x}', '\hat{y}', '\hat{z}')
local flatVars = {tHat,xHat,yHat,zHat}
local r, theta, phi = symmath.vars('r, '\\theta', '\\phi')
local eta = Tensor{indexes='_IJ', values=Matrix.identity(#flatVars)}
local u = Tensor('^I', table.unpack(symmath.Vector.sphericalToCartesian(r, theta, phi)))
local e = Tensor{indexes='_u^I', values=u'^I_,u':simplify())
local g = (e'_u^I' * e'_v^J' * eta'_IJ')
local manifold = Tensor.Manifold()
local curvedChart = manifold:Chart{coords=curvedVars, symbols='a-z', metric=function() return g end} -- 4D curved space
local flatChart = manifold:Chart{coords=flatVars, symbols='I-N', metric=function() return eta end} -- 4D Minkowski space
--TODO This part ...
manifold:setTransforms{from=curvedSpace, to=flatSpcae, transform=e}
Metric.setTransforms would be the new function
transform= would specify the tensor to transform between coordinate systems
a convention would have to be established such that ...
(for a-h the system from and i-n the system to)
e_a^i e_b^j eta_ij = g_ab
e_a^i e_b^j g^ab = eta_ij
e^a_i e^b_j eta^ij = g^ab
e^a_i e^b_j g_ab = eta_ij
e_a^i v^a = v^i transforms v upper from a->i
e_a^i v_i = v_a transforms v lower from i->a
e^a_i v_a = v_i transforms v lower from a->i
e^a_i v^i = v^a transforms v upper from i->a
it turns out if you do the math, you just need to provide one transform from 'from' to 'to'
then the inverse accomplishes the other direction,
and between uppers and lowers, raising/lowering metrics accordingly
(in the correct coordinate system) gets you what you want
the thing to be careful of is index order.
http://physics.stackexchange.com/questions/142836/correct-tetrad-index-notation
e_u^I != e^I_u no matter how many papers this is incorrectly stated in
typical linear transforms in index notation have the 'from' as the second coordinate
and the 'to' as the first: y_i = a_ij x_j
typical vielbein representation follow the same:
metric transformsations: g_uv = e_u^I e_v^J eta_IJ
inverse metric transformations: eta^IJ = g^uv e_u^I e_v^J
by some linear math we find that, once this is specified,
we can find the inverse transform by either
(1) compute the transpose inverse of the transform metric
(2) raise/lower indexes (i.e. multiply rhs by 'from' metric and lhs by 'to' metric inverse)
also:
Tensor.tangentSpaceOperators{diff(t), diff(x), diff(y), diff(z)}
or equivalent for commutation basis
but that only makes sense in combination with a chart...
and can be used to derive the metric
which, alternatively, we can just ignore the tangent space operators and allow the metric to be specified
and then instead of transforms, use "Chart" objects, and forward/inverse transforms ...
local M = Tensor.Manifold()
-- manifolds have multiple charts. how to handle that? how to make sure the multiple charts overlap wrt their respective coordinate bounds?
--
local C = Tensor.Chart{
manifold = M,
coordinates (or "variables"?) = {x1, x2, ..., xN},
-- optional
-- if chart isn't provided then a metric should be.
chartEmbeddedFunction = function(x)
-- in order to even build this, a separate set of coordinates / embedded space needs to be defined
-- or do I need a set of indexes for embedded variables?
-- I could reproduce that behavior with just an array of expressions and an inner product function
return Tensor('^I', function(x1, ...) ... end)
end,
-- accepts chart() output a and b, returns an expression for the inner product
-- optional, required for chart() (or should it be optional of chart() is provided as a Tensor of another chart)
chartInnerProduct = function(a,b)
local i = var'i'
-- TODO this isn't possible now, since __index doesn't return an expression itself
-- but it is possible in Maxima ... so ... maybe I should support it?
return Sum(a[i] * b[i], i, 1, N)
end,
-- or optionally, should I just provide embeddedChart() that returns a Tensor in the chart's coordinates?
-- and then the inner product is already defined in the embedded chart's space
-- but in that case, how to handle indexes?
-- in presence of a chart (and basis operators), the metric can be calculated
-- in absence of it, metric can be provided to describe the manifold behavior
metric = function(),
-- this will either be a Tensor (though creating a Tensor means the coordinates are specified, so the ctor know how many dimensions to require for each of its indexes)
-- or this can be Tensor args, or a function that generates the metric (most flexible?)
return Tensor('_uv', ...)
end,
-- default operators would be coordinate derivatives
-- the comma index will defer to this
-- honestly 'tangentSpaceOperators' is the same as overriding Variable:applyDiff
-- which is better? should 'applyDiff' even be a thing, since the only place it is used is in Ref for evaluating comma derivative indexes?
-- so that's a thing: replace Variable:applyDiff() and Tensor.coords{{variables=...}} with Chart.tangentSpaceOperators[]
tangentSpaceOperators = {
function(u, x1, x2, ...) return u:diff(x1) end,
function(u, x1, x2, ...) return u:diff(x2) end,
...
},
}
-- returns a (1 0) tensor-of-variables whose domain is the chart (subject to constraints of chart coordinate boundaries)
local x = C:point()
-- returns ... what? a list of operators? a list of basis vectors?
local Tx = C:tangentSpace(x)
-- and then for creating tensors, who do I request?
-- the manifold? nah, it could have multiple charts/multiple coordinates per chart
-- the chart? maybe
-- the tangent space? since the tensor exists within the tangent space.
-- but then we end up - just to get rolling - having to make all these things first: manifold, chart, point, tangent-space ...
-- and the point will be made of variables anyways so ... it is implicit already in the variables provided to the Chart constructor
-- so, probably, C:tensor(...) => shorthand for Tensor{..., chart=C}
indexes is a question of its own though, since I have *) indexes and *) variables,
and the two act separately for now, but there is crossover in usage when it comes to derivatives (comma deriv vs :diff())
and I want to unify the two somehow, especially wrt the Variable:setDependantVars() functionality ...
... I want to bring that over to the comma deriv and indexes so that comma deriv can implicitly know when to simplify to zero or not (or who knows, even expand to transforms)
so how would that look?
same as it already does? C ctor "symbols='IJKLMN'" ?
and then (store somewhere?) that those tensor index symbols are now associated with this chart,
so that comma using those symbols will simplify based on the letter's variables + the chart's coordinate variables
--]]
local table = require 'ext.table'
local range = require 'ext.range'
local assert = require 'ext.assert'
local Expression = require 'symmath.Expression'
local Array = require 'symmath.Array'
local symmath
--[[
general-purpose degree-1 (successive nesting for degree-n) structure
to be used as vectors, vectors of them as matrices, etc ...
--]]
local Tensor = Array:subclass()
Tensor.name = 'Tensor'
-- Array is non-commutative
-- because the child class Matrix is non-commutative
-- and frankly, how is multipliction of arrays well-defined?
-- but because Tensor has indexes, now we can commute our objects before carrying out multiplication
Tensor.mulNonCommutative = false
-- namespace:
Tensor.Ref = require 'symmath.tensor.Ref'
Tensor.Index = require 'symmath.tensor.Index'
Tensor.Manifold = require 'symmath.tensor.Manifold'
Tensor.Chart = require 'symmath.tensor.Chart'
Tensor.LeviCivita = require 'symmath.tensor.LeviCivita'
Tensor.DenseCache = require 'symmath.tensor.DenseCache'
-- TODO finishme
Tensor.dual = require 'symmath.tensor.dual'
--[[
how to deal with index symbols
by default use a-z for index symbols
honestly nothing in Tensor uses this, only Expression for its Tensor index stuff applied to all expressions
where to put this?
I was thinking put it in Chart, because Chart is defined with sets of symbols
I was thinking Manifold, since Manifold has lookup for chart based on symbol
I was thinking Tensor, since Tensor is the namespace of all of this
--]]
Tensor.defaultSymbols = require 'symmath.tensor.symbols'.latinSymbols
--[[
TODO Tensor construction:
- (optional) first argument tensor variance
- string that is parsed: '_ijk', etc
- table-of-TensorIndex objects (already in processed form that is returned by parseIndexes)
- aka args.indexes
- (optional) n-many dimension numbers of lua numbers or Constants
- aka args.dim
- (optional) final argument value generator
- function that accepts n parameters (for n degree of tensor)
- table that is n nestings deep (for n degree of tensor)
- aka args.values
information the constructor needs...
possible combinations:
* * * / contra/covariant + index information (includes variance and dimensions, excludes optional values)
* * \ list of dimension (excludes variance and optional values)
* * / dense content: expressions as nested tables (includes dimensions, excludes variance)
* * \ lambdas for content generation (includes values, excludes dimension or variance)
... NOTICE I'm getting rid of non-dense .. I'll just have deferred indexes on *all* expressions, variables included, which will serve the same
now that index evaluation is deferred, value-less tensors can be represented as variables with unevaluated index dereferences:
g = var'g'; g'_ij' <- will give you a variable with unevaluated dereference _ij.
evaluating it will cause an error, unless you substitute g for a proper Tensor object
constructors:
contra/co-variant alone:
Tensor(string)
Tensor'^i' = contravariant degree-1
Tensor'_ij' = covariant degree-2
Tensor'^i_jk' = mixed degree-3
default goes to ... contra? co? or neither / separate associated metric?
associate indexes with metrics?
functions for converting from/to different basii?
contra/co-variant + dense values:
Tensor(string, table)
Tensor('^i', {1,2,3}) = contravariant degree-3 tensor w/initial values
(error upon mismatch sizes, or only use what you can / fill the rest with zero?)
contra/co-variant + sparse values:
Tensor(string, function)
Tensor('^ij', function(i,j) return ... end)
dimensions:
Tensor(number...) <- conflict with the dense value definition
dimensions + lambda:
Tensor(number..., function)
dense content:
Tensor([number|table]...) <- conflict with dimensions constructor
interpretations:
Tensor(string) => contra/co-variance
Tensor(string, function) => contra/co-variance + lambda callback
Tensor(string, table) => contra/co-variance + dense value
Tensor(table, function) => contra/co-variance + lambda callback
Tensor(table, table) => contra/co-variance + dense values
Tensor(number...) => dense values
Tensor{dim=table, values=table} => dimension list + lambda callback
Tensor{dim=table} => dimension list
Tensor{}
Tensor static members:
- association of indicies to coordinates
manifold = Tensor.Manifold()
chart = manifold:Chart{coords={t,x,y,z}}
spatialChart = manifold:Chart{symbols='ijk', coords={x,y,z}}
cartesianChart = manifold:Chart{symbols='IJK', coords={whatever flat space vielbein indices you want to use}}
- coordinate transformation information ...
i.e. lower txyz to upper txyz basis transforms with g^uv,
upper txyz to lower txyz transforms with g_uv
lower txyz to lower TXYZ transforms with e_I^u, etc
Tensor have the following attributes:
- degree (list of dimensions) <- right now dynamcially calculated via :degree()
- list of associated basis (contra-/co-/neither)
- associated indices / index ranges? g_uv spans txyz vs g_ij spans xyz
--]]
function Tensor:init(...)
symmath = symmath or require 'symmath.namespace'()
local Constant = symmath.Constant
local TensorIndex = self.Index
local args = {...}
local argsAreNamed = type(args[1]) == 'table'
and (type(args[1].dim) == 'table'
or type(args[1].indexes) == 'table'
or type(args[1].indexes) == 'string')
local valueCallback
if type(args[#args]) == 'function' then
valueCallback = table.remove(args)
elseif argsAreNamed then
valueCallback = args[1].values
end
--[[
Tensor{[dim={dim1, dim2, ..., dimN}][, values=function(x1,...,xN) ... end)][, indexes={...}]}
either dim or indexes must be used
--]]
if argsAreNamed then
-- one of these two variables should be defined:
self.variance = args[1].indexes and TensorIndex.parseIndexes(args[1].indexes) or {}
local dim = args[1].dim
--if dim and args[1].indexes then error("can't specify dim and indexes") end
if dim then
assert.type(dim, 'table')
dim = range(#dim):map(function(i)
local di = dim[i]
if Constant:isa(di) then di = di.value end
assert.type(di, 'number')
return di
end)
-- construct content from default of zeroes
local subdim = table(dim)
local thisdim = subdim:remove(1)
local superArgs = {}
for i=1,thisdim do
if #subdim > 0 then
local subindexes = self.variance and setmetatable(table.sub(self.variance, 2), nil) or nil
superArgs[i] = Tensor{indexes=subindexes, dim=subdim}
else
superArgs[i] = Constant(0)
end
end
Expression.init(self, table.unpack(superArgs))
else
-- construct content from default of zeroes
local subVariance = table(self.variance)
local firstVariance = table.remove(subVariance, 1)
local chart = self:findChartForSymbol(firstVariance.symbol)
assert(chart, "looks like you haven't created a chart yet, so I don't know how to determine the dimension of the indexes")
local superArgs = {}
for i=1,#chart.coords do
if #subVariance > 0 then
superArgs[i] = Tensor(subVariance)
else
superArgs[i] = Constant(0)
end
end
Expression.init(self, table.unpack(superArgs))
end
else
--[[
Tensor'^i'
Tensor'_jk'
Tensor'^a_bc'
--]]
-- got a string of indexes
if type(args[1]) == 'string'
-- got an array of TensorIndexes
or (type(args[1]) == 'table' and TensorIndex:isa(args[1][1]))
then
local indexes = table.remove(args, 1)
-- *) parse string into indicies (and what chart they belong to) and contra- vs co- variance
-- should I make a distinction for multi-letter variables? not allowed for the time being ...
self.variance = TensorIndex.parseIndexes(indexes)
-- *) complain if there is no Tensor.coords assignment
-- *) store index information (in this tensor and subtensors ... i.e. this may be {^i, _j, _k}, subtensors would be {_j, _k}, and their subtensors would be {_k}
-- *) build an empty tensor with degree according to the chart size of the indices
if #args > 0 then
-- assert that the sizes are correct
local subVariance = table(self.variance)
table.remove(subVariance, 1)
-- convert-to-tensor before Expression.init,
-- because now Expression.init can't handle non-Expression vanilla tables
-- but if had any coerced types (number -> Constant) then we now have to coerce them here as well ...
for i=1,#args do
local x = args[i]
if Constant.isNumber(x) then
x = Constant(x)
args[i] = x
end
assert.type(x, 'table', "tensors can only be constructed with Expressions or tables of Expressions")
if not Expression:isa(x) then
-- then assume it's meant to be a sub-tensor
x = Tensor(subVariance, table.unpack(x))
args[i] = x
end
end
-- TODO if Tensor is constructed with Lua tables then Expression.init will complain
Expression.init(self, table.unpack(args))
else
-- construct content from default of zeroes
local subVariance = table(self.variance)
local firstVariance = table.remove(subVariance, 1)
local chart = self:findChartForSymbol(firstVariance.symbol)
local superArgs = {}
for i=1,#chart.coords do
if #subVariance > 0 then
superArgs[i] = Tensor(subVariance)
else
superArgs[i] = Constant(0)
end
end
Expression.init(self, table.unpack(superArgs))
end
--[[
Tensor({row1}, {row2}, ...)
--]]
else
-- if we get a list of tables then call super init ...
Expression.init(self, ...)
-- default: covariant?
-- TODO create defaults according to children (from the Expression.init(self, ...) call)
self.variance = {}
-- now that children are stored, construct them as lower-degree objects if the arguments were provided implicitly as metatable-less tables
-- this way we know all children (a) are Tensors and have a ".degree" field, or (b) are non-Tensor Expressions and are degree-0
for i=1,#self do
local x = self[i]
if Constant.isNumber(x) then
x = Constant(x)
args[i] = x
end
assert.type(x, 'table', "tensors can only be constructed with Expressions or tables of Expressions")
if not Expression:isa(x) then
-- then assume it's meant to be a sub-tensor
x = Tensor(table.unpack(x))
self[i] = x
end
end
end
end
if valueCallback then
symmath = symmath or require 'symmath.namespace'()
local clone = symmath.clone
for index,_ in self:iter() do
if type(valueCallback) == 'function' then
self[index] = clone(valueCallback(table.unpack(index)))
elseif Array:isa(valueCallback) then
self[index] = clone(valueCallback[index])
end
end
end
end
function Tensor:clone(...)
local copy = Tensor.super.clone(self, ...)
for i=1,#self.variance do
copy.variance[i] = self.variance[i]:clone()
end
return copy
end
--[=[
function Tensor.match(a, b, matches)
if not Tensor.super.match(a, b, matches) then return false end
--[[
assert(#a.variance == #b.variance)
for i=1,#a.variance do
if a.variance ~= b.variance then return false end
end
--]]
return (matches[1] or true), table.unpack(matches, 2, table.maxn(matches))
end
--]=]
--[[
produce a trace between dimensions i and j
store the result in dimension i, removing dimension j
--]]
function Tensor:trace(i,j)
symmath = symmath or require 'symmath.namespace'()
if i == j then
error("cannot apply contraction across the same index: "..i)
end
local dim = self:dim()
if dim[i] ~= dim[j] then
error("tried to apply tensor contraction across indices of differing dimension: "..i.."th and "..j.."th of "..table.concat(dim, ','))
end
local newdim = table(dim)
-- remove the second index from the new dimension
table.remove(newdim,j)
-- keep track of where the first index is in the new dimension
local newdimI = i
if j < i then newdimI = newdimI - 1 end
local newVariance = {table.unpack(self.variance)}
table.remove(newVariance, j)
return Tensor{
indexes = newVariance,
dim = newdim,
values = function(...)
local indexes = {...}
-- now when we reference the unremoved dimension
local srcIndexes = {table.unpack(indexes)}
table.insert(srcIndexes, j, indexes[newdimI])
return self:get(srcIndexes)
end,
}
end
--[[
this removes the i'th dimension, summing across it
if it removes the last dim then a number is returned (rather than a 0-degree tensor, which I don't support)
--]]
function Tensor:contraction(i)
symmath = symmath or require 'symmath.namespace'()
local dim = self:dim()
if i < 1 or i > #dim then error("tried to contract dimension "..i.." when we are only degree "..#dim) end
-- if there's a valid contraction and we're degree-1 then we're summing across everything
if #dim == 1 then
local result
for j=1,dim[1] do
if not result then
result = self[j]
else
result = result + self[j]
end
end
return result
end
local newdim = table(dim)
local removedDim = table.remove(newdim,i)
local newVariance = {table.unpack(self.variance)}
table.remove(newVariance, i)
return Tensor{
indexes = newVariance,
dim = newdim,
values = function(...)
local indexes = {...}
table.insert(indexes, i, 1)
local result
for index=1,removedDim do
indexes[i] = index
if not result then
result = self:get(indexes)
else
result = result + self:get(indexes)
end
end
return result
end,
}
end
function Tensor:simplifyTraces()
local modified
repeat
modified = false
for i=1,#self.variance-1 do
for j=i+1,#self.variance do
if self.variance[i].symbol == self.variance[j].symbol then
self = self:trace(i,j):contraction(i)
if not Tensor:isa(self) then
return self:simplify() -- if it's a scalar then return
end
modified = true
break
end
end
if modified then break end
end
until not modified
return self:simplify()
end
--[[
for all permutations of indexes other than i,
take each vector composed of index i
transform it by the provided degree-2 tensor
and store it back where you got it from
--]]
function Tensor:transformIndex(ti, m)
local dim = self:dim()
local mdim = m:dim()
if m:degree() ~= 2 then error("can only transform an index by a degree-2 metric, got a degree "..m:degree()) end
if mdim[1] ~= mdim[2] then error("can only transform an index by a square metric, got dims "..mdim:concat', ') end
if dim[ti] ~= mdim[1] then error("tried to transform tensor of dims "..dim:concat', '.." with metric of dims "..mdim:concat', ') end
return Tensor{dim=dim, values=function(...)
-- current element being transformed
local is = {...}
local vxi = is[ti] -- the current coordinate along the vector being transformed
local result = 0
for vi=1,mdim[1] do
local vis = {table.unpack(is)}
vis[ti] = vi
result = result + m:get{vxi, vi} * self:get(vis)
end
return result
end}
end
function Tensor:applyRaiseOrLower(i, tensorIndex)
local t = self:clone()
-- TODO this matches Tensor:__call
local srcChart = self:findChartForSymbol(t.variance[i].symbol)
local dstChart = self:findChartForSymbol(tensorIndex.symbol)
-- TODO what if the tensor was created without variance?
if tensorIndex.lower ~= t.variance[i].lower then
-- how do we handle raising indexes of subsets
local metric = (dstChart and dstChart.metric) or (srcChart and srcChart.metric)
local metricInverse = (dstChart and dstChart.metricInverse) or (srcChart and srcChart.metricInverse)
if not metric then
error("tried to raise/lower an index without a metric:"..tostring(self))
end
local tdim = t:dim()
if tdim[i] ~= metric:dim()[1]
or tdim[i] ~= metricInverse:dim()[1]
then
print("can't raise/lower index "..i.." until you set the metric tensor to one with dimension matching the tensor you are attempting to raise/lower")
print(i.."'th dim")
print(" your tensor's dimensions: "..table.concat(tdim, ','))
print(" metric dimensions: "..table.concat(metric:dim(),','))
print(" metric inverse dimensions: "..table.concat(metricInverse:dim(),','))
error("you can reset the metric tensor via the Chart:setMetric() function")
end
-- TODO generalize transforms, including inter-basis-symbol-sets
local oldVariance = table.mapi(t.variance, function(v) return v:clone() end)
if tensorIndex.lower and not t.variance[i].lower then
t = t:transformIndex(i, metric)
elseif not tensorIndex.lower and t.variance[i].lower then
t = t:transformIndex(i, metricInverse)
else
error("don't know how to raise/lower these indexes")
end
symmath = symmath or require 'symmath.namespace'()
oldVariance[i].lower = tensorIndex.lower
t = symmath.simplify(t)
-- ...now apply the new variance to all sub-tensors
local function applyVariance(t, variance)
t.variance = table(variance):mapi(function(var) return var:clone() end)
if Tensor:isa(t[1]) then
local subVariance = t.variance:sub(2)
assert.gt(#subVariance, 0, "somehow tensor .variance isn't as big as tensor degree")
for i,ti in ipairs(t) do
applyVariance(ti, subVariance)
end
end
end
applyVariance(t, oldVariance)
end
return t
end
-- permute the tensor's elements according to the dest variance
-- TODO rename to something that makes more sense? :form() or something, idk,
-- this function is used for reshaping internal form and index ordering
-- maybe 'permute' is good. maybe 'form' is good.
-- maybe 'reshape'
-- maybe I should have (variance string, tensor) as ctors for Tensor()'s and then permute them there, but then what about 1x1x1 tensor initialization?
function Tensor:permute(dstVariance)
if type(dstVariance) == 'string' then
local TensorIndex = self.Index
dstVariance = TensorIndex.parseIndexes(dstVariance)
end
--DEBUG(@5):print('dstVariance', table.mapi(dstVariance, tostring):concat())
-- determine index remapping
local indexMap = {}
for i,srcVar in ipairs(self.variance) do
indexMap[i] = table.find(dstVariance, nil, function(dstVar)
return srcVar.symbol == dstVar.symbol
end)
if not indexMap[i] then
error("assigning tensor with '"..srcVar.symbol.."' to tensor without that symbol: "..self)
end
end
local olddim = self:dim()
--DEBUG(@5):print('olddim', require 'ext.tolua'(olddim))
local newdim = {}
for i=1,#olddim do
newdim[indexMap[i]] = olddim[i]
end
--DEBUG(@5):print('newdim', require 'ext.tolua'(newdim))
-- perform assignment
local success, result = xpcall(function()
return Tensor{
indexes = dstVariance,
dim = newdim,
values = function(...)
local dstIndex = {...}
local srcIndex = {}
for i=1,#dstIndex do
srcIndex[i] = dstIndex[indexMap[i]]
end
return self:get(srcIndex)
end,
}
end, function(err)
return "failed for tensor "..self.."\n"
.. "when converting it to variance "..table.mapi(dstVariance, tostring):concat', '.."\n"
..err..'\n'..debug.traceback()
end)
if not success then error(result) end
return result
end
-- have to be copied?
-- TODO make this and call identical
-- ... or not. __call was moved to Expression so expressions could be indexed
Tensor.__index = function(self, key)
-- parent class access
local metavalue = getmetatable(self)[key]
if metavalue then return metavalue end
-- get a nested element
if type(key) == 'table' then
return self:get(key)
--elseif type(key) == 'string' then -- TODO interpret index notation
end
-- self class access
local value = rawget(self, key)
if value then return value end
-- last fallback to __call
-- return self.__call(self, key)
end
Tensor.__newindex = function(self, key, value)
-- TODO if value is a Tensor.Ref then run the Tensor.Ref prune transformation ...
-- I don't think I do much assignment-by-table ...
-- except for in the Visitor.lookupTable ...
-- otherwise, looks like it's not allowed in Arrays, where I've overridden it to be the setter
if type(key) == 'table' then
self:set(key, value)
return
end
-- handle assignment by tensor indexes
if type(key) == 'string'
and (key:sub(1,1) == '^' or key:sub(1,1) == '_')
then
local TensorIndex = self.Index
local dstVariance = TensorIndex.parseIndexes(key)
-- assert no comma derivatives
for _,dstVar in ipairs(dstVariance) do
assert(not dstVar.derivative, "can't assign to a partial derivative tensor")
end
-- if we're assigning a non-tensor to a tensor
-- then implicitly wrap it in a tensor
if not Tensor:isa(value) then
symmath = symmath or require 'symmath.namespace'()
local clone = symmath.clone
value = Tensor(dstVariance, function(...) return clone(value) end)
end
-- [====[ ok now I'm going to do this in :prune()
-- ... but what about subtensor assignment?
-- or was that pruning indexes in Tensor ctor?
-- did I just not do a thorough enough job in Tensor ctor?
--[[
for all non-number indexes
gather all variables of each of those indexes
iterate across all
... or, alternatively, wrap all single-variable dstVariance indexes that don't show up in value's variance
but what if there are two instances of the same single-variable?
you will have to mark them off as you find them ...
first find which single-variable indexes exist in both
then, if there are any left in 'dstVariance', wrap 'value' in those
without this, g['_tt'] = 2 will fail ... and must be written g['_tt'] = Tensor('_tt', 2)
or g['_ti'] = A'_i' will fail ... and must be written g['_ti'] = Tensor('_ti', A)
--]]
--DEBUG(@5):print('value variance',table.unpack(value.variance))
--DEBUG(@5):print('dest variance',table.unpack(dstVariance))
local function mapSingleIndexes(v,k,t)
--DEBUG(@5):print('v.symbol',v.symbol)
local chart = self:findChartForSymbol(v.symbol)
--DEBUG(@5):print('chart',chart,'variables',chart and #chart.coords)
return (chart and #chart.coords == 1 and k or nil), #t+1
end
local valueSingleVarIndexes = table.mapi(value.variance, mapSingleIndexes)
local dstSingleVarIndexes = table.mapi(dstVariance, mapSingleIndexes)
--DEBUG(@5):print('value single vars',valueSingleVarIndexes:unpack())
--DEBUG(@5):print('self single vars',dstSingleVarIndexes:unpack())
for _,dstIndex in ipairs(dstSingleVarIndexes) do
local v = dstVariance[dstIndex]
local k = valueSingleVarIndexes:find(nil, function(valueIndex) return v.symbol == value.variance[valueIndex].symbol end)
if k then
valueSingleVarIndexes:remove(k)
else
-- wrap it in the single-variable index
--DEBUG(@5):print('from ',value)
value = Tensor(table{
TensorIndex{
lower = v.lower,
derivative = not assert(not v.derivative),
symbol = v.symbol,
}
}:append(value.variance), value)
-- since we're wrapping the value tensor in a new index, increment all the index indexes
for i=1,#valueSingleVarIndexes do
valueSingleVarIndexes[i] = valueSingleVarIndexes[i] + 1
end
--DEBUG(@5):print('to ',value)
end
end
-- if any are left then remove them
if #valueSingleVarIndexes > 0 then
--DEBUG(@5):print('we still have '..#valueSingleVarIndexes..' left of ',table.mapi(value.variance,tostring):concat',',' at ',valueSingleVarIndexes:unpack())
value = Tensor(
-- remove the rest of the single-variance letters
table.filter(value.variance, function(v,k)
return not valueSingleVarIndexes:find(k)
end), function(...)
local is = {...}
for i=#valueSingleVarIndexes,1,-1 do
table.insert(is, valueSingleVarIndexes[i], 1)
end
return value[is]
end)
end
--]====]
--[====[ TODO what was I about to do here?
for _,variance in ipairs(dstVariance) do
local chart = self:findChartForSymbol(variance.symbol)
if #chart.coords == 1 then
local variable = chart.coords[1]
end
end
--]====]
-- simplify any expressions ... automatically here?
--if not Tensor:isa(value) then value = value:simplify() end
-- permute the indexes of the value to match the source
-- TODO no need to permute it if the index is entirely variables/numbers, such that the assignment is to a single element in the tensor
--DEBUG(@5):print('permuting...')
local dst = value:permute(dstVariance)
--DEBUG(@5):for i=1,#dst do print('dst['..i..']', dst[i]) end
-- reform self to the original variances
-- TODO once again for scalar assignment or subset assignment
--DEBUG(@5):print('applying variance...')
dst = dst(self.variance)
--DEBUG(@5):for i=1,#dst do print('dst['..i..']=', dst[i]) end
--DEBUG(@5):print('simplifying...')
dst = dst()
--DEBUG(@5):print('assigning from dst\n'..dst)
-- applying variance to dst puts dst into dst[1] because the subindex isn't there ...
--DEBUG(@5):print('all dst iters:')
--DEBUG(@5):for is in dst:iter() do print(table.concat(is, ',')) end
--DEBUG(@5):print('...done')
--[[ copy in new values
for is in self:iter() do
--DEBUG(@5):print('index is',table.concat(is, ','), ' assigning '..dst[is]..' to '..self[is])
self[is] = dst[is]
end
--]]
-- [[ only copy over values in the dst variance
for isrc in self:iter() do
-- isrc holds the iter of assignment ... might not be assigned if dstVariance doesn't hold a basis that holds the var
-- isrc[i] cooresponds to the i'th variable in srcChart.coords
-- then we find the same variable in dstChart.coords
-- if it isrc there - read from that variable - and write back to self.iter
-- if it isn't there - skip this iter
--DEBUG(@5):print('assigning to indexes '..table.concat(isrc, ','))
assert.eq(#isrc, #dstVariance)
-- looks similar to transformIndexes in Tensor/Ref.lua
local indexes = dstVariance
local notfound = false
for i=1,#indexes do
-- don't worry about raising or lowering
local srcChart = self:findChartForSymbol(self.variance[i].symbol)
local dstChart = self:findChartForSymbol(indexes[i].symbol)
--DEBUG(@5):print('assigning from '..indexes[i].symbol)
--DEBUG(@5):print('assigning into '..self.variance[i].symbol)
do--if srcIndex ~= dstIndex then
--DEBUG(@5):print('looking for', srcChart.coords[isrc[i]])
--DEBUG(@5):print('...among variables',table.unpack(dstChart.coords))
local dstIndex = table.find(dstChart.coords, srcChart.coords[isrc[i]])
-- however 'dst' has already been transformed to the basis of 'src' ...
-- ... and padded with zeros (TODO don't bother do that?)
-- so I don't need to reindex the lookup, just skip the zeroes
--DEBUG(@5):print('dstIndex',dstIndex)
if not dstIndex then
notfound = true
break
end
end
end
if not notfound then
self[isrc] = dst[isrc]
end
end
--]]
if #value.variance ~= #self.variance then
error("can't assign tensors of mismatching number of indexes")
end
return
end
rawset(self, key, value)
end
--[[
hmm, how to lookup charts from tensors
TODO Tensor.chart would be nice
--]]
function Tensor:findChartForSymbol(symbol)
local manifold = self.manifold
or (self.chart and self.chart.manifold)
or Tensor.Manifold.last
if not manifold then
return --error("don't know what Manifold to use associated with this symbol ... have you built a Manifold yet?")
end
return manifold:findChartForSymbol(symbol)
end
function Tensor.pruneAdd(lhs,rhs)
if not Tensor:isa(lhs) or not Tensor:isa(rhs) then return end
-- reorganize the elements of rhs so the letters match lhs
rhs = rhs:permute(lhs.variance)
-- TODO complain if the raise/lower doesn't match up for each index?
return Tensor{
indexes = lhs.variance,
dim = lhs:dim(),
values = function(...)
local indexes = {...}
return lhs:get(indexes) + rhs:get(indexes)
end,
}
end
function Tensor.pruneMul(lhs, rhs)
symmath = symmath or require 'symmath.namespace'()
local lhsIsArray = Array:isa(lhs)
local rhsIsArray = Array:isa(rhs)
local lhsIsTensor = Tensor:isa(lhs)
local rhsIsTensor = Tensor:isa(rhs)
local lhsIsScalar = not lhsIsTensor and not lhsIsArray
local rhsIsScalar = not rhsIsTensor and not rhsIsArray
assert(lhsIsTensor or rhsIsTensor)
if lhsIsTensor and rhsIsTensor then
--[[ old way (7.5 seconds on my problem term)
-- tensor-tensor mul
local result = Tensor{
indexes = table():append(lhs.variance):append(rhs.variance),
dim = table():append(lhs:dim()):append(rhs:dim()),
values = function(...)