diff options
Diffstat (limited to 'macros/luatex/latex/luacas/tex')
59 files changed, 11596 insertions, 0 deletions
diff --git a/macros/luatex/latex/luacas/tex/_lib/inspect.lua b/macros/luatex/latex/luacas/tex/_lib/inspect.lua new file mode 100644 index 0000000000..dedd91f248 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/_lib/inspect.lua @@ -0,0 +1,335 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.1.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique GarcĂa Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +local tostring = tostring + +inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) +inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + +local function rawpairs(t) + return next, t, nil +end + +-- Apostrophizes the string if it has quotes, but not aphostrophes +-- Otherwise, it returns a regular quoted string +local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' +end + +-- \a => '\\a', \0 => nil +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127", +} +local longControlCharEscapes = {["\127"]="\127"} -- \a => nil, \0 => \000, 31 => \031 +for i=0, 31 do + local ch = string.char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\"..i + longControlCharEscapes[ch] = string.format("\\%03d", i) + end +end +--longControlCharEscapes["\127"]="\\127" + +local function escape(str) + return (str:gsub("\\", "\\\\") + :gsub("(%c)%f[0-9]", longControlCharEscapes) + :gsub("%c", shortControlCharEscapes)) +end + +local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) +end + +local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb +end + +-- For implementation reasons, the behavior of rawlen & # is "undefined" when +-- tables aren't pure sequences. So we implement our own # operator. +local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 +end + +local function getNonSequentialKeys(t) + local keys, keysLength = {}, 0 + local sequenceLength = getSequenceLength(t) + for k,_ in rawpairs(t) do + if not isSequenceKey(k, sequenceLength) then + keysLength = keysLength + 1 + keys[keysLength] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLength, sequenceLength +end + +local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in rawpairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances +end + +local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len +end + +local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath +end + +local function processRecursive(process, item, path, visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k,v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + + + +------------------------------------------------------------------- + +local Inspector = {} +local Inspector_mt = {__index = Inspector} + +function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = args[i] + end +end + +function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 +end + +function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) +end + +function Inspector:alreadyVisited(v) + return self.ids[v] ~= nil +end + +function Inspector:getId(v) + local id = self.ids[v] + if not id then + local tv = type(v) + id = (self.maxIds[tv] or 0) + 1 + self.maxIds[tv] = id + self.ids[v] = id + end + return tostring(id) +end + +function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") +end + +function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('<table ', self:getId(t), '>') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + + self:puts('{') + self:down(function() + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for i=1, nonSequentialKeysLength do + local k = nonSequentialKeys[i] + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if type(mt) == 'table' then + if count > 0 then self:puts(',') end + self:tabify() + self:puts('<metatable> = ') + self:putValue(mt) + end + end) + + if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end +end + +function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<', tv, ' ', self:getId(v), '>') + end +end + +------------------------------------------------------------------- + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local inspector = setmetatable({ + depth = depth, + level = 0, + buffer = {}, + ids = {}, + maxIds = {}, + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) +end + +setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + +return inspect + diff --git a/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua b/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua new file mode 100644 index 0000000000..4717543c4b --- /dev/null +++ b/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua @@ -0,0 +1,629 @@ +---@diagnostic disable: param-type-mismatch, lowercase-global, need-check-nil, assign-type-mismatch +--[[ + +== Introduction == + + Note that this requires os.clock(), debug.sethook(), + and debug.getinfo() or your equivalent replacements to + be available if this is an embedded application. + + Example usage: + + profiler = newProfiler() + profiler:start() + + < call some functions that take time > + + profiler:stop() + + local outfile = io.open( "profile.txt", "w+" ) + profiler:report( outfile ) + outfile:close() + +== Optionally choosing profiling method == + +The rest of this comment can be ignored if you merely want a good profiler. + + newProfiler(method, sampledelay): + +If method is omitted or "time", will profile based on real performance. +optionally, frequency can be provided to control the number of opcodes +per profiling tick. By default this is 100000, which (on my system) provides +one tick approximately every 2ms and reduces system performance by about 10%. +This can be reduced to increase accuracy at the cost of performance, or +increased for the opposite effect. + +If method is "call", will profile based on function calls. Frequency is +ignored. + + +"time" may bias profiling somewhat towards large areas with "simple opcodes", +as the profiling function (which introduces a certain amount of unavoidable +overhead) will be called more often. This can be minimized by using a larger +sample delay - the default should leave any error largely overshadowed by +statistical noise. With a delay of 1000 I was able to achieve inaccuray of +approximately 25%. Increasing the delay to 100000 left inaccuracy below my +testing error. + +"call" may bias profiling heavily towards areas with many function calls. +Testing found a degenerate case giving a figure inaccurate by approximately +20,000%. (Yes, a multiple of 200.) This is, however, more directly comparable +to common profilers (such as gprof) and also gives accurate function call +counts, which cannot be retrieved from "time". + +I strongly recommend "time" mode, and it is now the default. + +== History == + +2021-01-04 - Larry Deaton ( larry.deaton@dynetics.com ) + Modified the profiling by "call" operation to ignore internal LUA functions + since LUA only provides debug hooks for the calling of the function and not + the return of the function. Without this change, the call stack is + continually growing. + +2008-09-16 - Time-based profiling and conversion to Lua 5.1 + by Ben Wilhelm ( zorba-pepperfish@pavlovian.net ). + Added the ability to optionally choose profiling methods, along with a new + profiling method. + +Converted to Lua 5, a few improvements, and +additional documentation by Tom Spilman ( tom@sickheadgames.com ) + +Additional corrections and tidying by original author +Daniel Silverstone ( dsilvers@pepperfish.net ) + +== Status == + +Daniel Silverstone is no longer using this code, and judging by how long it's +been waiting for Lua 5.1 support, I don't think Tom Spilman is either. I'm +perfectly willing to take on maintenance, so if you have problems or +questions, go ahead and email me :) +-- Ben Wilhelm ( zorba-pepperfish@pavlovian.net ) ' + +== Copyright == + +Lua profiler - Copyright Pepperfish 2002,2003,2004 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to +do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +--]] + + +-- +-- All profiler related stuff is stored in the top level table '_profiler' +-- +_profiler = {} + + +-- +-- newProfiler() creates a new profiler object for managing +-- the profiler and storing state. Note that only one profiler +-- object can be executing at one time. +-- +function newProfiler(variant, sampledelay) + if _profiler.running then + print("Profiler already running.") + return + end + + variant = variant or "time" + + if variant ~= "time" and variant ~= "call" then + print("Profiler method must be 'time' or 'call'.") + return + end + + local newprof = {} + for k,v in pairs(_profiler) do + newprof[k] = v + end + newprof.variant = variant + newprof.sampledelay = sampledelay or 100000 + return newprof +end + + +-- +-- This function starts the profiler. It will do nothing +-- if this (or any other) profiler is already running. +-- +function _profiler.start(self) + if _profiler.running then + return + end + -- Start the profiler. This begins by setting up internal profiler state + _profiler.running = self + self.rawstats = {} + self.callstack = {} + if self.variant == "time" then + self.lastclock = os.clock() + debug.sethook( _profiler_hook_wrapper_by_time, "", self.sampledelay ) + elseif self.variant == "call" then + debug.sethook( _profiler_hook_wrapper_by_call, "cr" ) + else + print("Profiler method must be 'time' or 'call'.") + end +end + + +-- +-- This function stops the profiler. It will do nothing +-- if a profiler is not running, and nothing if it isn't +-- the currently running profiler. +-- +function _profiler.stop(self) + if _profiler.running ~= self then + return + end + -- Stop the profiler. + debug.sethook( nil ) + _profiler.running = nil +end + + +-- +-- Simple wrapper to handle the hook. You should not +-- be calling this directly. Duplicated to reduce overhead. +-- +function _profiler_hook_wrapper_by_call(action) + if _profiler.running == nil then + debug.sethook( nil ) + end + _profiler.running:_internal_profile_by_call(action) +end +function _profiler_hook_wrapper_by_time(action) + if _profiler.running == nil then + debug.sethook( nil ) + end + _profiler.running:_internal_profile_by_time(action) +end + + +-- +-- This is the main by-function-call function of the profiler and should not +-- be called except by the hook wrapper +-- +function _profiler._internal_profile_by_call(self,action) + -- Since we can obtain the 'function' for the item we've had call us, we + -- can use that... + local caller_info = debug.getinfo( 3 ) + if caller_info == nil then + print "No caller_info" + return + end + + if caller_info.short_src == "[C]" then + -- LMD -- These are builtin functions and for some reason the + -- debug hook does not catch the return of these functions. + -- So, for now, we are just going to skip them. + return + end + + --SHG_LOG("[_profiler._internal_profile] "..(caller_info.name or "<nil>")) + + -- Retrieve the most recent activation record... + local latest_ar = nil + if #self.callstack > 0 then + latest_ar = self.callstack[#self.callstack] + end + + -- Are we allowed to profile this function? + local should_not_profile = 0 + for k,v in pairs(self.prevented_functions) do + if k == caller_info.func then + should_not_profile = v + end + end + -- Also check the top activation record... + if latest_ar then + if latest_ar.should_not_profile == 2 then + should_not_profile = 2 + end + end + + -- Now then, are we in 'call' or 'return' ? + -- print("Profile:", caller_info.name, "SNP:", should_not_profile, + -- "Action:", action ) + if action == "call" then + -- Making a call... + local this_ar = {} + this_ar.should_not_profile = should_not_profile + this_ar.parent_ar = latest_ar + this_ar.anon_child = 0 + this_ar.name_child = 0 + this_ar.children = {} + this_ar.children_time = {} + this_ar.clock_start = os.clock() + -- Last thing to do on a call is to insert this onto the ar stack... + table.insert( self.callstack, this_ar ) + else + local this_ar = latest_ar + if this_ar == nil then + return -- No point in doing anything if no upper activation record + end + + -- Right, calculate the time in this function... + this_ar.clock_end = os.clock() + this_ar.this_time = this_ar.clock_end - this_ar.clock_start + + -- Now, if we have a parent, update its call info... + if this_ar.parent_ar then + this_ar.parent_ar.children[caller_info.func] = + (this_ar.parent_ar.children[caller_info.func] or 0) + 1 + this_ar.parent_ar.children_time[caller_info.func] = + (this_ar.parent_ar.children_time[caller_info.func] or 0 ) + + this_ar.this_time + if caller_info.name == nil then + this_ar.parent_ar.anon_child = + this_ar.parent_ar.anon_child + this_ar.this_time + else + this_ar.parent_ar.name_child = + this_ar.parent_ar.name_child + this_ar.this_time + end + end + -- Now if we're meant to record information about ourselves, do so... + if this_ar.should_not_profile == 0 then + local inforec = self:_get_func_rec(caller_info.func,1) + inforec.count = inforec.count + 1 + inforec.time = inforec.time + this_ar.this_time + inforec.anon_child_time = inforec.anon_child_time + this_ar.anon_child + inforec.name_child_time = inforec.name_child_time + this_ar.name_child + inforec.func_info = caller_info + for k,v in pairs(this_ar.children) do + inforec.children[k] = (inforec.children[k] or 0) + v + inforec.children_time[k] = + (inforec.children_time[k] or 0) + this_ar.children_time[k] + end + end + + -- Last thing to do on return is to drop the last activation record... + table.remove( self.callstack, #self.callstack) + end +end + + +-- +-- This is the main by-time internal function of the profiler and should not +-- be called except by the hook wrapper +-- +function _profiler._internal_profile_by_time(self,action) + -- we do this first so we add the minimum amount of extra time to this call + local timetaken = os.clock() - self.lastclock + + local depth = 3 + local at_top = true + local last_caller + local caller = debug.getinfo(depth) + while caller do + if not caller.func then caller.func = "(tail call)" end + if self.prevented_functions[caller.func] == nil then + local info = self:_get_func_rec(caller.func, 1, caller) + info.count = info.count + 1 + info.time = info.time + timetaken + if last_caller then + -- we're not the head, so update the "children" times also + if last_caller.name then + info.name_child_time = info.name_child_time + timetaken + else + info.anon_child_time = info.anon_child_time + timetaken + end + info.children[last_caller.func] = + (info.children[last_caller.func] or 0) + 1 + info.children_time[last_caller.func] = + (info.children_time[last_caller.func] or 0) + timetaken + end + end + depth = depth + 1 + last_caller = caller + caller = debug.getinfo(depth) + end + + self.lastclock = os.clock() +end + + +-- +-- This returns a (possibly empty) function record for +-- the specified function. It is for internal profiler use. +-- +function _profiler._get_func_rec(self,func,force,info) + -- Find the function ref for 'func' (if force and not present, create one) + local ret = self.rawstats[func] + if ret == nil and force ~= 1 then + return nil + end + if ret == nil then + -- Build a new function statistics table + ret = {} + ret.func = func + ret.count = 0 + ret.time = 0 + ret.anon_child_time = 0 + ret.name_child_time = 0 + ret.children = {} + ret.children_time = {} + ret.func_info = info + self.rawstats[func] = ret + end + return ret +end + + +-- +-- This writes a profile report to the output file object. If +-- sort_by_total_time is nil or false the output is sorted by +-- the function time minus the time in it's children. +-- +function _profiler.report( self, outfile, sort_by_total_time ) + + outfile:write + [[Lua Profile output created by profiler.lua. Copyright Pepperfish 2002+ + +]] + + -- This is pretty awful. + local terms = {} + if self.variant == "time" then + terms.capitalized = "Sample" + terms.single = "sample" + terms.pastverb = "sampled" + elseif self.variant == "call" then + terms.capitalized = "Call" + terms.single = "call" + terms.pastverb = "called" + else + assert(false) + end + + local total_time = 0 + local ordering = {} + for func,record in pairs(self.rawstats) do + table.insert(ordering, func) + end + + if sort_by_total_time then + table.sort( ordering, + function(a,b) return self.rawstats[a].time > self.rawstats[b].time end + ) + else + table.sort( ordering, + function(a,b) + local arec = self.rawstats[a] + local brec = self.rawstats[b] + local atime = arec.time - (arec.anon_child_time + arec.name_child_time) + local btime = brec.time - (brec.anon_child_time + brec.name_child_time) + return atime > btime + end + ) + end + + for i=1,#ordering do + local func = ordering[i] + local record = self.rawstats[func] + local thisfuncname = " " .. self:_pretty_name(func) .. " " + if string.len( thisfuncname ) < 42 then + thisfuncname = + string.rep( "-", (42 - string.len(thisfuncname))/2 ) .. thisfuncname + thisfuncname = + thisfuncname .. string.rep( "-", 42 - string.len(thisfuncname) ) + end + + total_time = total_time + ( record.time - ( record.anon_child_time + + record.name_child_time ) ) + outfile:write( string.rep( "-", 19 ) .. thisfuncname .. + string.rep( "-", 19 ) .. "\n" ) + outfile:write( terms.capitalized.." count: " .. + string.format( "%4d", record.count ) .. "\n" ) + outfile:write( "Time spend total: " .. + string.format( "%4.3f", record.time ) .. "s\n" ) + outfile:write( "Time spent in children: " .. + string.format("%4.3f",record.anon_child_time+record.name_child_time) .. + "s\n" ) + local timeinself = + record.time - (record.anon_child_time + record.name_child_time) + outfile:write( "Time spent in self: " .. + string.format("%4.3f", timeinself) .. "s\n" ) + outfile:write( "Time spent per " .. terms.single .. ": " .. + string.format("%4.5f", record.time/record.count) .. + "s/" .. terms.single .. "\n" ) + outfile:write( "Time spent in self per "..terms.single..": " .. + string.format( "%4.5f", timeinself/record.count ) .. "s/" .. + terms.single.."\n" ) + + -- Report on each child in the form + -- Child <funcname> called n times and took a.bs + local added_blank = 0 + for k,v in pairs(record.children) do + if self.prevented_functions[k] == nil or + self.prevented_functions[k] == 0 + then + if added_blank == 0 then + outfile:write( "\n" ) -- extra separation line + added_blank = 1 + end + outfile:write( "Child " .. self:_pretty_name(k) .. + string.rep( " ", 41-string.len(self:_pretty_name(k)) ) .. " " .. + terms.pastverb.." " .. string.format("%6d", v) ) + outfile:write( " times. Took " .. + string.format("%4.2f", record.children_time[k] ) .. "s\n" ) + end + end + + outfile:write( "\n" ) -- extra separation line + outfile:flush() + end + outfile:write( "\n\n" ) + outfile:write( "Total time spent in profiled functions: " .. + string.format("%5.3g",total_time) .. "s\n" ) + outfile:write( [[ + +END +]] ) + outfile:flush() +end + + +-- +-- This writes the profile to the output file object as +-- loadable Lua source. +-- +function _profiler.lua_report(self,outfile) + -- Purpose: Write out the entire raw state in a cross-referenceable form. + local ordering = {} + local functonum = {} + for func,record in pairs(self.rawstats) do + table.insert(ordering, func) + functonum[func] = #ordering + end + + outfile:write( + "-- Profile generated by profiler.lua Copyright Pepperfish 2002+\n\n" ) + outfile:write( "-- Function names\nfuncnames = {}\n" ) + for i=1,#ordering do + local thisfunc = ordering[i] + outfile:write( "funcnames[" .. i .. "] = " .. + string.format("%q", self:_pretty_name(thisfunc)) .. "\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Function times\nfunctimes = {}\n" ) + for i=1,#ordering do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "functimes[" .. i .. "] = { " ) + outfile:write( "tot=" .. record.time .. ", " ) + outfile:write( "achild=" .. record.anon_child_time .. ", " ) + outfile:write( "nchild=" .. record.name_child_time .. ", " ) + outfile:write( "count=" .. record.count .. " }\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child links\nchildren = {}\n" ) + for i=1,#ordering do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "children[" .. i .. "] = { " ) + for k,v in pairs(record.children) do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( functonum[k] .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child call counts\nchildcounts = {}\n" ) + for i=1,#ordering do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "children[" .. i .. "] = { " ) + for k,v in record.children do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( v .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child call time\nchildtimes = {}\n" ) + for i=1,#ordering do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc]; + outfile:write( "children[" .. i .. "] = { " ) + for k,v in pairs(record.children) do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( record.children_time[k] .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n\n-- That is all.\n\n" ) + outfile:flush() +end + +-- Internal function to calculate a pretty name for the profile output +function _profiler._pretty_name(self,func) + + -- Only the data collected during the actual + -- run seems to be correct.... why? + local info = self.rawstats[ func ].func_info + -- local info = debug.getinfo( func ) + + local name = "" + if info.what == "Lua" then + name = "L:" + end + if info.what == "C" then + name = "C:" + end + if info.what == "main" then + name = " :" + end + + if info.name == nil then + name = name .. "<"..tostring(func) .. ">" + else + name = name .. info.name + end + + if info.source then + name = name .. "@" .. info.source + else + if info.what == "C" then + name = name .. "@?" + else + name = name .. "@<string>" + end + end + name = name .. ":" + if info.what == "C" then + name = name .. "?" + else + name = name .. info.linedefined + end + + return name +end + + +-- +-- This allows you to specify functions which you do +-- not want profiled. Setting level to 1 keeps the +-- function from being profiled. Setting level to 2 +-- keeps both the function and its children from +-- being profiled. +-- +-- BUG: 2 will probably act exactly like 1 in "time" mode. +-- If anyone cares, let me (zorba) know and it can be fixed. +-- +function _profiler.prevent(self, func, level) + self.prevented_functions[func] = (level or 1) +end + + +_profiler.prevented_functions = { + [_profiler.start] = 2, + [_profiler.stop] = 2, + [_profiler._internal_profile_by_time] = 2, + [_profiler._internal_profile_by_call] = 2, + [_profiler_hook_wrapper_by_time] = 2, + [_profiler_hook_wrapper_by_call] = 2, + [_profiler.prevent] = 2, + [_profiler._get_func_rec] = 2, + [_profiler.report] = 2, + [_profiler.lua_report] = 2, + [_profiler._pretty_name] = 2 +}
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/_lib/table.lua b/macros/luatex/latex/luacas/tex/_lib/table.lua new file mode 100644 index 0000000000..70495d1479 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/_lib/table.lua @@ -0,0 +1,185 @@ +-- Checks if two arrays are equal, starting at index i +function ArrayEqual(a1, a2, i) + i = i or 1 + while i <= math.max(#a1, #a2) do + if a1[i] ~= a2[i] then + return false + end + i = i + 1 + end + return true +end + +-- Checks if two arrays are equal, the first starting at index i, the second starting at index j +function FancyArrayEqual(a1, a2, i, j) + i = i or 1 + j = j or 1 + while i <= math.max(#a1, #a2) or j <= math.max(#a1, #a2) do + if a1[i] ~= a2[j] then + return false + end + i = i + 1 + j = j + 1 + end + return true +end + +-- Creates a copy of a table +function Copy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in pairs(orig) do + copy[orig_key] = orig_value + end + copy = setmetatable(copy, getmetatable(orig)) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +-- Joins two arrays +function JoinArrays(a1, a2) + local a = Copy(a1) + for index, value in ipairs(a2) do + a[index + #a1] = value + end + return a +end + +-- Joins two arrays indexed from zero +function JoinZeroArrays(a1, a2) + local a = Copy(a1) + a[#a1 + 1] = a2[0] + for index, value in ipairs(a2) do + a[index + #a1 + 1] = value + end + return a +end + +-- Join two tables, using the second entry if a key appears in both tables +function JoinTables(t1, t2) + local t = Copy(t1) or {} + for key, value in pairs(t2) do + t[key] = value + end + return t +end + +-- Given an array, removes all occurances of that element from the array +function Remove(a, e) + local r = Copy(a) + local found = false + for index, value in ipairs(r) do + if e == value then + found = true + end + if found then + r[index] = r[index + 1] + end + end + return r +end + +-- Given an array, removes all elements in the second array from the first +function RemoveAll(a1, a2) + local r = Copy(a1) + local removed = 0 + for index, value in ipairs(r) do + if a2[value] then + removed = removed + 1 + end + if removed then + r[index] = r[index + removed] + end + end + return r +end + +-- Given an array of arrays, returns only the arrays that have no elements in common with the second array +function RemoveAny(aa, a) + local toremove = {} + for _, v in ipairs(aa) do + for _, v1 in ipairs(v) do + for _, v2 in ipairs(a) do + if v1 == v2 then + toremove[#toremove+1] = v + goto endcheck + end + end + end + ::endcheck:: + end + return RemoveAll(aa, toremove) +end + +-- Converts an array to a string recursively +function ToStringArray(t) + if string.sub(tostring(t), 1, 6) == "table:" then + local out = "{" + for index, value in ipairs(t) do + out = out .. ToStringArray(value) + if t[index + 1] then + out = out .. ", " + end + end + return out .. "}" + end + return tostring(t) +end + +-- Converts a table to a string recursively +function ToStringTable(t) + if string.sub(tostring(t), 1, 6) == "table:" then + local out = "{" + for index, value in pairs(t) do + out = out .. ToStringTable(index) .. " : " .. ToStringTable(value) + if t[index + 1] then + out = out .. ", " + end + end + return out .. "}" + end + return tostring(t) +end + +-- Check if a table contains an element, and returns the index of that element if it does +function Contains(t, e) + for index, value in pairs(t) do + if value == e then + return index + end + end + return false +end + +-- Given an array a of unique elements, returns an array of the n-element subarrays of a +function Subarrays(a, m) + local aout = {} + local l = 1 + local newmax = {} + + if(m <= 0) then + return {{}}, {0} + end + + local rec, max = Subarrays(a, m - 1) + + for recindex, set in pairs(rec) do + for index, element in pairs(a) do + if not set[element] and index > max[recindex] then + local new = Copy(set) + new[#new+1] = element + aout[l] = new + if not newmax[l] or index > newmax[l] then + newmax[l] = index + end + l = l + 1 + end + end + end + + return aout, newmax +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/_init.lua b/macros/luatex/latex/luacas/tex/algebra/_init.lua new file mode 100644 index 0000000000..0a9b114cb9 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/_init.lua @@ -0,0 +1,24 @@ +-- Loads algebra files in the correct order. +require("_lib.table") + +require("core._init") + +require("algebra.ring") +require("algebra.euclideandomain") +require("algebra.field") +require("algebra.polynomialring") +require("algebra.integer") +require("algebra.rational") +require("algebra.integerquotientring") +require("algebra.sqrtexpression") + +require("algebra.absexpression") +require("algebra.equation") +require("algebra.factorialexpression") +require("algebra.logarithm") +require("algebra.rootexpression") +require("algebra.trigexpression") + +require("algebra.polynomialring.berlekampfactoring") +require("algebra.polynomialring.zassenhausfactoring") +require("algebra.polynomialring.decomposition")
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/absexpression.lua b/macros/luatex/latex/luacas/tex/algebra/absexpression.lua new file mode 100644 index 0000000000..2bf0b6a7ef --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/absexpression.lua @@ -0,0 +1,80 @@ +--- @class AbsExpression +--- The absolute value of an expression. +--- @field expression Expression +AbsExpression = {} +__AbsExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new absolute value expression with the given expression. +--- @param expression Expression +--- @return AbsExpression +function AbsExpression:new(expression) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.expression = expression + + __o.__index = AbsExpression + __o.__tostring = function(a) + return '|' .. tostring(a.expression) .. '|' + end + + o = setmetatable(o, __o) + return o +end + +--- @return Expression +function AbsExpression:evaluate() + if self.expression:isconstant() then + if self.expression >= Integer.zero() then + return self.expression + end + return -self.expression + end + return self +end + +--- @return Expression +function AbsExpression:autosimplify() + return AbsExpression(self.expression:autosimplify()):evaluate() +end + +--- @return table<number, Expression> +function AbsExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return AbsExpression +function AbsExpression:setsubexpressions(subexpressions) + return AbsExpression(subexpressions[1]) +end + +--- @param other Expression +--- @return boolean +function AbsExpression:order(other) + return FunctionExpression("abs", self.expression):order(other) +end + +--- @return string +function AbsExpression:tolatex() + return "\\left|" .. self.expression:tolatex() .. "\\right|" +end + +----------------- +-- Inheritance -- +----------------- + +__AbsExpression.__index = CompoundExpression +__AbsExpression.__call = AbsExpression.new +AbsExpression = setmetatable(AbsExpression, __AbsExpression) + +---------------------- +-- Static constants -- +---------------------- +ABS = function (a) + return AbsExpression(a) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/equation.lua b/macros/luatex/latex/luacas/tex/algebra/equation.lua new file mode 100644 index 0000000000..4e81d5ca95 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/equation.lua @@ -0,0 +1,183 @@ +--- @class Equation +--- An expression that represents an equation of the form lhs = rhs. +--- @field lhs Expression +--- @field rhs Expression +Equation = {} +__Equation = {} + +-------------------------- +-- Static functionality -- +-------------------------- + +--- Attempts to isolate the variable var in lhs by moving expressions to rhs. Ony performs a single step. +--- @param lhs Expression +--- @param rhs Expression +--- @param var SymbolExpression +--- @return Expression, Expression +function Equation.isolatelhs(lhs, rhs, var) + if lhs:type() == BinaryOperation then + local stay = Integer.zero() + local switch = Integer.zero() + if lhs.operation == BinaryOperation.ADD then + for _, exp in ipairs(lhs:subexpressions()) do + if exp:freeof(var) then + switch = switch + exp + else + stay = stay + exp + end + end + if switch == Integer.zero() then + lhs = lhs:factor() -- TODO: Replace with collect for efficiency reasons + else + return stay:autosimplify(), (rhs - switch):autosimplify() + end + end + if lhs.operation == BinaryOperation.MUL then + stay = Integer.one() + switch = Integer.one() + for _, exp in ipairs(lhs:subexpressions()) do + if exp:freeof(var) then + switch = switch * exp + else + stay = stay * exp + end + end + return stay:autosimplify(), (rhs / switch):autosimplify() + end + if lhs.operation == BinaryOperation.POW then + if lhs:subexpressions()[1]:freeof(var) then + return lhs:subexpressions()[2]:autosimplify(), Logarithm(lhs:subexpressions()[1], rhs):autosimplify() + elseif lhs:subexpressions()[2]:freeof(var) then + return lhs:subexpressions()[1]:autosimplify(), (rhs ^ (Integer.one()/lhs:subexpressions()[2])):autosimplify() + end + end + elseif lhs:type() == Logarithm then + if lhs.base:freeof(var) then + return lhs.expression:autosimplify(), (lhs.base ^ rhs):autosimplify() + elseif lhs.expression:freeof(var) then + return lhs.base:autosimplify(), (lhs.expression ^ (Integer.one()/rhs)):autosimplify() + end + elseif lhs:type() == TrigExpression then + return lhs.expression:autosimplify(), TrigExpression(TrigExpression.INVERSES[lhs.name], rhs):autosimplify() + end + + return lhs, rhs +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new equation with the given expressions. +--- @param lhs Expression +--- @param rhs Expression +--- @return Equation +function Equation:new(lhs, rhs) + + if lhs:type() == Equation or rhs:type() == Equation then + error("Sent parameter of wrong type: cannot nest equations or inequalities") + end + + local o = {} + local __o = Copy(__ExpressionOperations) -- TODO: Ensure only one metatable for each instance of a class + + o.lhs = lhs + o.rhs = rhs + + __o.__index = Equation + __o.__tostring = function(a) + return tostring(a.lhs) .. ' = ' .. tostring(a.rhs) + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not b:type() == Equation then + return false + end + return a.lhs == b.lhs and a.rhs == b.rhs + end + o = setmetatable(o, __o) + + return o +end + +--- Evaluation in this case just checks for structural equality, or guarenteed inequality in the case of constants +--- @return Equation|boolean +function Equation:evaluate() + if self.lhs == self.rhs then + return true -- TODO: Add Boolean Expressions + end + if self.lhs:isconstant() and self.rhs:isconstant() and self.lhs ~= self.rhs then + return false + end + return self +end + +--- @return Equation|boolean +function Equation:autosimplify() + local lhs = self.lhs:autosimplify() + local rhs = self.rhs:autosimplify() + + return Equation(lhs, rhs):evaluate() +end + +--- @return table<number, Expression> +function Equation:subexpressions() + return {self.lhs, self.rhs} +end + +--- Attempts to solve the equation for a particular variable. +--- @param var SymbolExpression +--- @return Equation +function Equation:solvefor(var) + local lhs = self.lhs + local rhs = self.rhs + + if lhs:freeof(var) and rhs:freeof(var) then + return self + end + + -- Check for monovariate polynomial expressions + local root = (lhs - rhs):autosimplify() + local poly, status = root:expand():topolynomial() + if status then + -- TODO: Add Set expressions + return Equation(var, poly:roots()[1]) + end + + local newlhs, newrhs = root, Integer(0) + local oldlhs + while newlhs ~= var and oldlhs ~= newlhs do + oldlhs = newlhs + newlhs, newrhs = Equation.isolatelhs(newlhs, newrhs, var) + end + + return Equation(newlhs, newrhs) +end + +--- @param subexpressions table<number, Expression> +--- @return Equation +function Equation:setsubexpressions(subexpressions) + return Equation(subexpressions[1], subexpressions[2]) +end + +--- @param other Expression +--- @return boolean +function Equation:order(other) + if other:isatomic() then + return false + end + + return self.lhs:order(other) +end + +--- @return string +function Equation:tolatex() + return self.lhs:tolatex() .. '=' .. self.rhs:tolatex() +end + +----------------- +-- Inheritance -- +----------------- +__Equation.__index = CompoundExpression +__Equation.__call = Equation.new +Equation = setmetatable(Equation, __Equation)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua b/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua new file mode 100644 index 0000000000..fab2c5c7c2 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua @@ -0,0 +1,63 @@ +--- @class EuclideanDomain +--- Interface for an element of a euclidean domain. +EuclideanDomain = {} +__EuclideanDomain = {} + +---------------------- +-- Required methods -- +---------------------- + +--- @param b EuclideanDomain +--- @return EuclideanDomain, EuclideanDomain +function EuclideanDomain:divremainder(b) + error("Called unimplemented method : divremainder()") +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- @return boolean +function EuclideanDomain:iscommutative() + return true +end + +-------------------------- +-- Instance metamethods -- +-------------------------- + +__EuclideanOperations = Copy(__RingOperations) + +-- Division with remainder +-- Unfortunately, this can only return 1 result, so it returns the quotient - for the remainder use a % b, or a:divremainder(b) +__EuclideanOperations.__idiv = function(a, b) + if(b == b:zero()) then + error("Cannot divide by zero.") + end + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to divide two elements of incompatable rings") + end + return a:inring(oring):divremainder(b:inring(oring)) +end + +__EuclideanOperations.__mod = function(a, b) + if(b == b:zero()) then + error("Cannot divide by zero.") + end + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to divide two elements of incompatable rings") + end + local _,q = a:inring(oring):divremainder(b:inring(oring)) + return q +end + +----------------- +-- Inheritance -- +----------------- + +__EuclideanDomain.__index = Ring +EuclideanDomain = setmetatable(EuclideanDomain, __EuclideanDomain)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua b/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua new file mode 100644 index 0000000000..1c5b824ff7 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua @@ -0,0 +1,102 @@ +--- @class FactorialExpression +--- The factorial of an expression. +--- @field expression Expression +FactorialExpression = {} +__FactorialExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new factorial expression with the given expression. +--- @param expression Expression +--- @return FactorialExpression +function FactorialExpression:new(expression) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.expression = expression + + __o.__index = FactorialExpression + __o.__tostring = function(a) + return '(' .. tostring(a.expression) .. ')!' + end + + o = setmetatable(o, __o) + return o +end + +--- @return Expression +function FactorialExpression:evaluate() + if self.expression:type() == Integer then + if self.expression < Integer.zero() then + error("Aritmetic Error: Factorials of negative integers are not defined.") + end + + if not FactorialExpression.LIMIT then + FactorialExpression.LIMIT = Integer(5000) + end + + if self.expression > FactorialExpression.LIMIT then + return self + end + -- TODO: More efficient factorial computations. + local out = Integer.one() + local i = Integer.zero() + while i < self.expression do + i = i + Integer.one() + out = out * i + end + return out + end + return self +end + +--- @return Expression +function FactorialExpression:autosimplify() + return FactorialExpression(self.expression:autosimplify()):evaluate() +end + +--- @return table<number, Expression> +function FactorialExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return AbsExpression +function FactorialExpression:setsubexpressions(subexpressions) + return FactorialExpression(subexpressions[1]) +end + +--- @param other Expression +--- @return boolean +function FactorialExpression:order(other) + return FunctionExpression("fact", self.expression):order(other) +end + +--- @return string +function FactorialExpression:tolatex() + if self.expression:isatomic() then + return self.expression:tolatex() .. "!" + end + return "(" .. self.expression:tolatex() .. ")!" +end + +----------------- +-- Inheritance -- +----------------- + +__FactorialExpression.__index = CompoundExpression +__FactorialExpression.__call = FactorialExpression.new +FactorialExpression = setmetatable(FactorialExpression, __FactorialExpression) + +---------------------- +-- Static constants -- +---------------------- + +-- Do not attempt to compute factorials larger than this. +FactorialExpression.LIMIT = Integer(5000) + +FACT = function (a) + return FactorialExpression(a) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/field.lua b/macros/luatex/latex/luacas/tex/algebra/field.lua new file mode 100644 index 0000000000..c845e94c55 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/field.lua @@ -0,0 +1,69 @@ +--- @class Field +--- Interface for an element of a field. +Field = {} +__Field = {} + +---------------------- +-- Required methods -- +---------------------- + +--- @return Field +function Field:div(b) + return self:mul(b:inv()) +end + +--- @return Field +function Field:inv() + error("Called unimplemented method: inv()") +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Field exponentiation based on the definition. Specific rings may implement more efficient methods. +--- @return Field +function Field:pow(n) + local base = self + if(n < Integer.zero()) then + n = -n + base = base:inv() + end + local k = Integer.zero() + local b = self.getring().one() + while k < n do + b = b.mul(base) + k = k + Integer.one() + end + return b +end + +-------------------------- +-- Instance metamethods -- +-------------------------- + +__FieldOperations = Copy(__EuclideanOperations) + +__FieldOperations.__div = function(a, b) + if not b.getring and not b:isconstant() then + return BinaryOperation.DIVEXP({a, b}) + end + + if(b == b:zero()) then + error("Arithmetic Error: Cannot divide by zero.") + end + + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to divide two elements of incompatable rings") + end + return a:inring(oring):div(b:inring(oring)) +end + +----------------- +-- Inheritance -- +----------------- + +__Field.__index = EuclideanDomain +Field = setmetatable(Field, __Field)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/integer.lua b/macros/luatex/latex/luacas/tex/algebra/integer.lua new file mode 100644 index 0000000000..a3c54f498c --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/integer.lua @@ -0,0 +1,952 @@ +--- @class Integer +--- Represents an element of the ring of integers. +--- @field self table<number, number> +--- @field sign number +Integer = {} +__Integer = {} + +-------------------------- +-- Static functionality -- +-------------------------- + +-- The length of each digit in base 10. 10^15 < 2^53 < 10^16, so 15 is the highest value that will work with double-percision numbers. +-- For multiplication to work properly, however, this also must be even so we can take the square root of the digit size exactly. +-- 10^14 is still larger than 2^26, so it is still efficient to do multiplication this way. +Integer.DIGITLENGTH = 14 +-- The maximum size for a digit. While this doesn't need to be a power of 10, it makes implementing converting to and from strings much easier. +Integer.DIGITSIZE = 10 ^ Integer.DIGITLENGTH +-- Partition size for multiplying integers so we can get both the upper and lower bits of each digits +Integer.PARTITIONSIZE = math.floor(math.sqrt(Integer.DIGITSIZE)) + +--- Method for computing the gcd of two integers using Euclid's algorithm. +--- @param a Integer +--- @param b Integer +--- @return Integer +function Integer.gcd(a, b) + while b ~= Integer.zero() do + a, b = b, a%b + end + return a +end + +--- Method for computing the gcd of two integers using Euclid's algorithm. +--- Also returns Bezout's coefficients via extended gcd. +--- @param a Integer +--- @param b Integer +--- @return Integer, Integer, Integer +function Integer.extendedgcd(a, b) + local oldr, r = a, b + local olds, s = Integer.one(), Integer.zero() + local oldt, t = Integer.zero(), Integer.one() + while r ~= Integer.zero() do + local q = oldr // r + oldr, r = r, oldr - q*r + olds, s = s, olds - q*s + oldt, t = t, oldt - q*t + end + return oldr, olds, oldt +end + +--- Method for computing the larger of two integers. +--- Also returns the other integer for sorting purposes. +--- @param a Integer +--- @param b Integer +--- @return Integer, Integer +function Integer.max(a, b) + if a > b then + return a, b + end + return b, a +end + +--- Method for computing the smaller of two integers. +--- Also returns the other integer for sorting purposes. +--- @param a Integer +--- @param b Integer +--- @return Integer, Integer +function Integer.min(a, b) + if a < b then + return a, b + end + return b, a +end + +--- Methods for computing the larger magnitude of two integers. +--- Also returns the other integer for sorting purposes, and the number -1 if the two values were swapped, 1 if not. +--- @param a Integer +--- @param b Integer +--- @return Integer, Integer, number +function Integer.absmax(a, b) + if b:ltabs(a) then + return a, b, 1 + end + return b, a, -1 +end + +-- Returns the ceiling of the log base (defaults to 10) of a. +-- In other words, returns the least n such that base^n > a. +--- @param a Integer +--- @param base Integer +--- @return Integer +function Integer.ceillog(a, base) + base = base or Integer(10) + local k = Integer.zero() + + while (base ^ k) < a do + k = k + Integer.one() + end + + return k +end + +--- Returns a ^ b (mod n). This should be used when a ^ b is potentially large. +--- @param a Integer +--- @param b Integer +--- @param n Integer +--- @return Integer +function Integer.powmod(a, b, n) + if n == Integer.one() then + return Integer.zero() + else + local r = Integer.one() + a = a % n + while b > Integer.zero() do + if b % Integer(2) == Integer.one() then + r = (r * a) % n + end + a = (a ^ Integer(2)) % n + b = b // Integer(2) + end + return r + end +end + +--- @return RingIdentifier +local t = {ring=Integer} +t = setmetatable(t, {__index = Integer, __eq = function(a, b) + return a["ring"] == b["ring"] +end, __tostring = function(a) + return "ZZ" +end}) +function Integer.makering() + return t +end + + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- So we don't have to copy the Euclidean operations each time we create an integer. +local __o = Copy(__EuclideanOperations) +__o.__index = Integer +__o.__tostring = function(a) -- Only works if the digit size is a power of 10 + local out = "" + for i, digit in ipairs(a) do + local pre = tostring(math.floor(digit)) + if i ~= #a then + while #pre ~= Integer.DIGITLENGTH do + pre = "0" .. pre + end + end + out = pre .. out + end + if a.sign == -1 then + out = "-" .. out + end + return out +end +__o.__div = function(a, b) -- Constructor for a rational number disguised as division + if not b.getring then + return BinaryOperation.DIVEXP({a, b}) + end + if(a:getring() == Integer:getring() and b:getring() == Integer:getring()) then + return Rational(a, b) + end + return __FieldOperations.__div(a, b) +end +__o.__concat = function(a, b) -- Like a decimal, but fancier. Used mainly for the parser with decimal numbers. + return a + b / (Integer(10) ^ Integer.ceillog(b)) +end + +--- Creates a new integer given a string or number representation of the integer. +--- @param n number|string|Integer +--- @return Integer +function Integer:new(n) + local o = {} + o = setmetatable(o, __o) + + if not n then + o[1] = 0 + o.sign = 0 + return o + end + + -- Can convert any floating-point number into an integer, though we generally only want to pass whole numbers into this. + -- This will only approximate very large floating point numbers to a small proportion of the total significant digits + -- After that the result will just be nonsense - strings should probably be used for big numbers + if type(n) == "number" then + n = math.floor(n) + if n == 0 then + o[1] = 0 + o.sign = 0 + else + if n < 0 then + n = -n + o.sign = -1 + else + o.sign = 1 + end + local i = 1 + while n >= Integer.DIGITSIZE do + o[i] = n % Integer.DIGITSIZE + n = n // Integer.DIGITSIZE + i = i + 1 + end + o[i] = n + end + -- Only works on strings that are exact (signed) integers + elseif type(n) == "string" then + if not tonumber(n) then + error("Sent parameter of wrong type: " .. n .. " is not an integer.") + end + if n == "0" then + o[1] = 0 + o.sign = 0 + else + local s = 1 + if string.sub(n, 1, 1) == "-" then + s = s + 1 + o.sign = -1 + else + o.sign = 1 + end + + while string.sub(n, s, s) == "0" do + s = s + 1 + end + + local e = #n + local i = 1 + while e > s + Integer.DIGITLENGTH - 1 do + o[i] = tonumber(string.sub(n, e - Integer.DIGITLENGTH + 1, e)) + e = e - Integer.DIGITLENGTH + i = i + 1 + end + o[i] = tonumber(string.sub(n, s, e)) or 0 + end + -- Copying is expensive in Lua, so this constructor probably should only sparsely be called with an Integer argument. + elseif type(n) == "table" then + o = Copy(n) + else + error("Sent parameter of wrong type: Integer does not accept " .. type(n) .. ".") + end + + return o +end + +--- Returns the ring this object is an element of. +--- @return RingIdentifier +function Integer:getring() + return t +end + +--- @param ring RingIdentifier +--- @return Ring +function Integer:inring(ring) + if ring == self:getring() then + return self + end + + if ring == PolynomialRing:getring() then + return PolynomialRing({self:inring(ring.child)}, ring.symbol) + end + + if ring == Rational:getring() then + if ring.child then + return Rational(self:inring(ring.child), self:inring(ring.child):one(), true) + end + return Rational(self, Integer.one(), true):inring(ring) + end + + if ring == IntegerModN:getring() then + return IntegerModN(self, ring.modulus) + end + + error("Unable to convert element to proper ring.") +end + +--- @param b Integer +--- @return Integer +function Integer:add(b) + if self.sign == 1 and b.sign == -1 then + return self:usub(b, 1) + end + if self.sign == -1 and b.sign == 1 then + return self:usub(b, -1) + end + + local sign = self.sign + if sign == 0 then + sign = b.sign + end + return self:uadd(b, sign) +end + +--- Addition without sign so we don't have to create an entire new integer when switching signs. +--- @param b Integer +--- @param sign number +--- @return Integer +function Integer:uadd(b, sign) + local o = Integer() + o.sign = sign + + local c = 0 + local n = math.max(#self, #b) + for i = 1, n do + local s = (self[i] or 0) + (b[i] or 0) + c + if s >= Integer.DIGITSIZE then + o[i] = s - Integer.DIGITSIZE + c = 1 + else + o[i] = s + c = 0 + end + end + if c == 1 then + o[n + 1] = c + end + return o +end + +--- @param b Integer +--- @return Integer +function Integer:sub(b) + if self.sign == 1 and b.sign == -1 then + return self:uadd(b, 1) + end + if self.sign == -1 and b.sign == 1 then + return self:uadd(b, -1) + end + + local sign = self.sign + if sign == 0 then + sign = b.sign + end + return self:usub(b, sign) +end + +-- Subtraction without sign so we don't have to create an entire new integer when switching signs. +-- Uses subtraction by compliments. +--- @param b Integer +--- @param sign number +--- @return Integer +function Integer:usub(b, sign) + local a, b, swap = Integer.absmax(self, b) + local o = Integer() + o.sign = sign * swap + + local c = 0 + local n = #a + for i = 1, n do + local s = (a[i] or 0) + Integer.DIGITSIZE - 1 - (b[i] or 0) + c + if i == 1 then + s = s + 1 + end + if s >= Integer.DIGITSIZE then + o[i] = s - Integer.DIGITSIZE + c = 1 + else + o[i] = s + c = 0 + end + end + + -- Remove leading zero digits, since we want integer representations to be unique. + while o[n] == 0 do + o[n] = nil + n = n - 1 + end + + if not o[1] then + o[1] = 0 + o.sign = 0 + end + + return o +end + +--- @return Integer +function Integer:neg() + local o = Integer() + o.sign = -self.sign + for i, digit in ipairs(self) do + o[i] = digit + end + return o +end + +--- @param b Integer +--- @return Integer +function Integer:mul(b) + local o = Integer() + o.sign = self.sign * b.sign + if o.sign == 0 then + o[1] = 0 + return o + end + + -- Fast single-digit multiplication in the most common case + if #self == 1 and #b == 1 then + o[2], o[1] = self:mulone(self[1], b[1]) + + if o[2] == 0 then + o[2] = nil + end + + return o + end + + -- "Grade school" multiplication algorithm for numbers with small numbers of digits works faster than Karatsuba + local n = #self + local m = #b + o[1] = 0 + o[2] = 0 + for i = 2, n+m do + o[i + 1] = 0 + for j = math.max(1, i-m), math.min(n, i-1) do + local u, l = self:mulone(self[j], b[i - j]) + o[i - 1] = o[i - 1] + l + o[i] = o[i] + u + if o[i - 1] >= Integer.DIGITSIZE then + o[i - 1] = o[i - 1] - Integer.DIGITSIZE + o[i] = o[i] + 1 + end + if o[i] >= Integer.DIGITSIZE then + o[i] = o[i] - Integer.DIGITSIZE + o[i + 1] = o[i + 1] + 1 + end + end + end + + -- Remove leading zero digits, since we want integer representations to be unique. + if o[n+m+1] == 0 then + o[n+m+1] = nil + end + + if o[n+m] == 0 then + o[n+m] = nil + end + + return o +end + +--- Multiplies two single-digit numbers and returns two digits. +--- @param a number +--- @param b number +--- @return number, number +function Integer:mulone(a, b) + local P = Integer.PARTITIONSIZE + + local a1 = a // P + local a2 = a % P + local b1 = b // P + local b2 = b % P + + local u = a1 * b1 + local l = a2 * b2 + + local m = ((a1 * b2) + (b1 * a2)) + local mu = m // P + local ml = m % P + + u = u + mu + l = l + ml * P + + if l >= Integer.DIGITSIZE then + l = l - Integer.DIGITSIZE + u = u + 1 + end + + return u, l +end + +--- Naive exponentiation is slow even for small exponents, so this uses binary exponentiation. +--- @param b Integer +--- @return Integer +function Integer:pow(b) + if b < Integer.zero() then + return Integer.one() / (self ^ -b) + end + + if b == Integer.zero() then + return Integer.one() + end + + -- Fast single-digit exponentiation + if #self == 1 and #b == 1 then + local test = (self.sign * self[1]) ^ b[1] + if test < Integer.DIGITSIZE and test > -Integer.DIGITSIZE then + return Integer(test) + end + end + + local x = self + local y = Integer.one() + while b > Integer.one() do + if b[1] % 2 == 0 then + x = x * x + b = b:divbytwo() + else + y = x * y + x = x * x + b = b:divbytwo() + end + end + + return x * y +end + +-- Fast integer division by two for binary exponentiation. +--- @return Integer +function Integer:divbytwo() + local o = Integer() + o.sign = self.sign + for i = #self, 1, -1 do + if self[i] % 2 == 0 then + o[i] = self[i] // 2 + else + o[i] = self[i] // 2 + if i ~= 1 then + o[i - 1] = self[i - 1] * 2 + end + end + end + return o +end + +--- Division with remainder over the integers. Uses the standard base 10 long division algorithm. +--- @param b Integer +--- @return Integer, Integer +function Integer:divremainder(b) + if self >= Integer.zero() and b > self or self <= Integer.zero() and b < self then + return Integer.zero(), Integer(self) + end + + if #self == 1 and #b == 1 then + return Integer((self[1]*self.sign) // (b[1]*b.sign)), Integer((self[1]*self.sign) % (b[1]*b.sign)) + end + + local Q = Integer() + local R = Integer() + + Q.sign = self.sign * b.sign + R.sign = 1 + local negativemod = false + if b.sign == -1 then + b.sign = -b.sign + negativemod = true + end + + for i = #self, 1, -1 do + local s = tostring(math.floor(self[i])) + while i ~= #self and #s ~= Integer.DIGITLENGTH do + s = "0" .. s + end + Q[i] = 0 + for j = 1, #s do + R = R:mulbyten() + R[1] = R[1] + tonumber(string.sub(s, j, j)) + if R[1] > 0 then + R.sign = 1 + end + while R >= b do + R = R - b + Q[i] = Q[i] + 10^(#s - j) + end + end + end + + -- Remove leading zero digits, since we want integer representations to be unique. + while Q[#Q] == 0 do + Q[#Q] = nil + end + + if negativemod then + R = -R + b.sign = -b.sign + elseif self.sign == -1 then + R = b - R + end + + return Q, R +end + +--- Fast in-place multiplication by ten for the division algorithm. This means the number IS MODIFIED by this method unlike the rest of the library. +--- @return Integer +function Integer:mulbyten() + local DIGITSIZE = Integer.DIGITSIZE + for i, _ in ipairs(self) do + self[i] = self[i] * 10 + end + for i, _ in ipairs(self) do + if self[i] > DIGITSIZE then + local msd = self[i] // DIGITSIZE + if self[i+1] then + self[i+1] = self[i+1] + msd + else + self[i+1] = msd + end + self[i] = self[i] - DIGITSIZE*msd + end + end + return self +end + +--- @param b Integer +--- @return boolean +function Integer:eq(b) + for i, digit in ipairs(self) do + if not b[i] or not (b[i] == digit) then + return false + end + end + return #self == #b and self.sign == b.sign +end + +--- @param b Integer +--- @return boolean +function Integer:lt(b) + local selfsize = #self + local bsize = #b + if selfsize < bsize then + return b.sign == 1 + end + if selfsize > bsize then + return self.sign == -1 + end + local n = selfsize + while n > 0 do + if self[n]*self.sign < b[n]*b.sign then + return true + end + if self[n]*self.sign > b[n]*b.sign then + return false + end + n = n - 1 + end + return false +end + +--- Same as less than, but ignores signs. +--- @param b Integer +--- @return boolean +function Integer:ltabs(b) + if #self < #b then + return true + end + if #self > #b then + return false + end + local n = #self + while n > 0 do + if self[n] < b[n] then + return true + end + if self[n] > b[n] then + return false + end + n = n - 1 + end + return false +end + +--- @param b Integer +--- @return boolean +function Integer:le(b) + local selfsize = #self + local bsize = #b + if selfsize < bsize then + return b.sign == 1 + end + if selfsize > bsize then + return self.sign == -1 + end + local n = selfsize + while n > 0 do + if self[n]*self.sign < b[n]*b.sign then + return true + end + if self[n]*self.sign > b[n]*b.sign then + return false + end + n = n - 1 + end + return true +end + +local zero = Integer:new(0) +--- @return Integer +function Integer:zero() + return zero +end + +local one = Integer:new(1) +--- @return Integer +function Integer:one() + return one +end + +--- Returns this integer as a floating point number. Can only approximate the value of large integers. +--- @return number +function Integer:asnumber() + local n = 0 + for i, digit in ipairs(self) do + n = n + digit * Integer.DIGITSIZE ^ (i - 1) + end + return self.sign*math.floor(n) +end + +--- Returns all positive divisors of the integer. Not guarenteed to be in any order. +--- @return table<number, Integer> +function Integer:divisors() + local primefactors = self:primefactorizationrec() + local divisors = {} + + local terms = {} + for prime in pairs(primefactors) do + if prime == Integer(-1) then + primefactors[prime] = nil + end + terms[prime] = Integer.zero() + end + + local divisor = Integer.one() + + while true do + divisors[#divisors+1] = divisor + for prime, power in pairs(primefactors) do + if terms[prime] < power then + terms[prime] = terms[prime] + Integer.one() + divisor = divisor * prime + break + else + terms[prime] = Integer.zero() + divisor = divisor / (prime ^ power) + end + end + if divisor == Integer.one() then + break + end + end + + return divisors +end + +--- Returns whether this integer is a prime power, of the form p^a for prime p and positive integer a. +--- If it is a prime power, also returns the prime and the power. +--- @return boolean, Expression|nil, Expression|nil +function Integer:isprimepower() + if self <= Integer.one() then + return false + end + local factorization = self:primefactorization() + if factorization:type() == BinaryOperation and #factorization:subexpressions() == 1 then + return true, factorization.expressions[1].expressions[2], factorization.expressions[1].expressions[1] + end + return false +end + +--- Returns whether this integer is a perfect power, of the form a^b for positive integers a and b. +--- If it is a prime power, also returns the prime and the power. +--- @return boolean, Expression|nil, Expression|nil +function Integer:isperfectpower() + if self <= Integer.one() then + return false + end + local factorization = self:primefactorization() + if factorization:type() ~= BinaryOperation then + return false + end + local power = Integer.zero() + for _, term in ipairs(factorization:subexpressions()) do + power = Integer.gcd(power, term.expressions[2]) + if power == Integer.one() then + return false + end + end + local base = Integer.one() + for _, term in ipairs(factorization:subexpressions()) do + base = base * term.expressions[1] ^ (term.expressions[2] / power) + end + return true, base, power +end + +--- Returns the prime factorization of this integer as a expression. +--- @return Expression +function Integer:primefactorization() + if not Integer.FACTORIZATIONLIMIT then + Integer.FACTORIZATIONLIMIT = Integer(Integer.DIGITSIZE) + end + if self > Integer.FACTORIZATIONLIMIT then + return self + end + local result = self:primefactorizationrec() + local mul = {} + local i = 1 + for factor, degree in pairs(result) do + mul[i] = BinaryOperation.POWEXP({factor, degree}) + i = i + 1 + end + return BinaryOperation.MULEXP(mul):lock(Expression.NIL) +end + +--- Recursive part of prime factorization using Pollard Rho. +function Integer:primefactorizationrec() + if self < Integer.zero() then + return Integer.mergefactors({[Integer(-1)]=Integer.one()}, (-self):primefactorizationrec()) + end + if self == Integer.one() then + return {[Integer.one()]=Integer.one()} + end + local result = self:findafactor() + if result == self then + return {[result]=Integer.one()} + end + local remaining = self / result + return Integer.mergefactors(result:primefactorizationrec(), remaining:primefactorizationrec()) +end + + +function Integer.mergefactors(a, b) + local result = Copy(a) + + for factor, degree in pairs(b) do + for ofactor, odegree in pairs(result) do + if factor == ofactor then + result[ofactor] = degree + odegree + goto continue + end + end + result[factor] = degree + ::continue:: + end + return result +end + +-- Return a non-trivial factor of n via Pollard Rho, or returns n if n is prime. +function Integer:findafactor() + if self:isprime() then + return self + end + + if self % Integer(2) == Integer.zero() then + return Integer(2) + end + + if self % Integer(3) == Integer.zero() then + return Integer(3) + end + + if self % Integer(5) == Integer.zero() then + return Integer(5) + end + + local g = function(x) + local temp = Integer.powmod(x, Integer(2), self) + return temp + end + + local xstart = Integer(2) + while xstart < self do + local x = xstart + local y = xstart + local d = Integer.one() + while d == Integer.one() do + x = g(x) + y = g(g(y)) + d = Integer.gcd((x - y):abs(), self) + end + + if d < self then + return d + end + + xstart = xstart + Integer.one() + end +end + +--- Uses Miller-Rabin to determine whether a number is prime up to a very large number. +local smallprimes = {Integer:new(2), Integer:new(3), Integer:new(5), Integer:new(7), Integer:new(11), Integer:new(13), Integer:new(17), +Integer:new(19), Integer:new(23), Integer:new(29), Integer:new(31), Integer:new(37), Integer:new(41), Integer:new(43), Integer:new(47)} + +function Integer:isprime() + if self % Integer(2) == Integer.zero() then + if self == Integer(2) then + return true + end + return false + end + + if self == Integer.one() then + return false + end + + for _, value in pairs(smallprimes) do + if value == self then + return true + end + end + + local r = Integer.zero() + local d = self - Integer.one() + while d % Integer(2) == Integer.zero() do + r = r + Integer.one() + d = d / Integer(2) + end + + for _, a in ipairs(smallprimes) do + local s = r + local x = Integer.powmod(a, d, self) + if x == Integer.one() or x == self - Integer.one() then + goto continue + end + + while s > Integer.zero() do + x = Integer.powmod(x, Integer(2), self) + if x == self - Integer.one() then + goto continue + end + s = s - Integer.one() + end + do + return false + end + ::continue:: + end + + return true +end + +--- Returns the absolute value of an integer. +--- @return Integer +function Integer:abs() + if self.sign >= 0 then + return Integer(self) + end + return -self +end + +----------------- +-- Inheritance -- +----------------- + +__Integer.__index = EuclideanDomain +__Integer.__call = Integer.new +Integer = setmetatable(Integer, __Integer) + +---------------------- +-- Static constants -- +---------------------- + +Integer.FACTORIZATIONLIMIT = Integer(Integer.DIGITSIZE)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua b/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua new file mode 100644 index 0000000000..4c2188c9e5 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua @@ -0,0 +1,197 @@ +--- @class IntegerModN +--- Represents an element of the ring of integers mod n (this is also a field iff n is prime). +--- @field element Integer +--- @field modulus Integer + +IntegerModN = {} +__IntegerModN = {} + +-- Metatable for ring objects. +local __obj = {__index = IntegerModN, __eq = function(a, b) + return a["ring"] == b["ring"] and (a["modulus"] == b["modulus"] or a["modulus"] == nil or b["modulus"] == nil) +end, __tostring = function(a) + if a.modulus then return "Z/Z" .. tostring(a.modulus) else return "(Generic Integer Mod Ring)" end +end} + +-------------------------- +-- Static functionality -- +-------------------------- + +--- Creates a new ring with the given modulus. +--- @param modulus Integer +--- @return RingIdentifier +function IntegerModN.makering(modulus) + local t = {ring = IntegerModN} + t.modulus = modulus + t = setmetatable(t, __obj) + return t +end + +-- Shorthand constructor for a ring with a particular modulus. +function IntegerModN.R(modulus) + return IntegerModN.makering(modulus) +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- So we don't have to copy the field operations each time +local __o +__o = Copy(__FieldOperations) + +__o.__index = IntegerModN +__o.__tostring = function(a) + return tostring(a.element) +end + +--- Creates a new integer i in Z/nZ. +--- @param i Integer +--- @param n Integer +--- @return IntegerModN +function IntegerModN:new(i, n) + local o = {} + + if n:getring() ~= Integer:getring() or n < Integer.one() then + error("Argument error: modulus must be an integer greater than 0.") + end + + o = setmetatable(o, __o) + + if i < Integer.zero() or i >= n then + i = i % n + end + + o.element = i + o.modulus = n + + return o +end + +--- @return RingIdentifier +function IntegerModN:getring() + local t = {ring = IntegerModN} + if self then + t.modulus = self.modulus + end + t = setmetatable(t, __obj) + return t +end + +--- @param ring RingIdentifier +--- @return Ring +function IntegerModN:inring(ring) + if ring == IntegerModN:getring() then + if ring.modulus then + return IntegerModN(self.element, ring.modulus) + end + return self + end + + if ring == PolynomialRing:getring() then + return PolynomialRing({self:inring(ring.child)}, ring.symbol) + end + + if ring == Rational:getring() and ring.symbol then + return Rational(self:inring(ring.child), self:inring(ring.child):one(), true) + end + + if ring == Integer:getring() then + return self.element:inring(ring) + end + + error("Unable to convert element to proper ring.") +end + +--- @param b IntegerModN +--- @return IntegerModN +function IntegerModN:add(b) + return IntegerModN(self.element + b.element, self.modulus) +end + +--- @return IntegerModN +function IntegerModN:neg() + return IntegerModN(-self.element, self.modulus) +end + +--- @param b IntegerModN +--- @return IntegerModN +function IntegerModN:mul(b) + return IntegerModN(self.element * b.element, self.modulus) +end + +-- Overrides the generic power method with powmod. +--- @param b IntegerModN +--- @return IntegerModN +function IntegerModN:pow(b) + return IntegerModN(Integer.powmod(self.element, b.element, self.modulus), self.modulus) +end + +-- Returns the multiplicative inverse of this number if it exists. +--- @return IntegerModN +function IntegerModN:inv() + local r, t, _ = Integer.extendedgcd(self.element, self.modulus) + + if r > Integer.one() then + error("Element does not have an inverse in this ring") + end + + return IntegerModN(t, self.modulus) +end + +--- @param b IntegerModN +--- @return IntegerModN +function IntegerModN:div(b) + return self:mul(b:inv()) +end + +--- @param b IntegerModN +--- @return boolean +function IntegerModN:eq(b) + return self.element == b.element +end + +--- @param b IntegerModN +--- @return boolean +function IntegerModN:lt(b) + return self.element < b.element +end + +--- @param b IntegerModN +--- @return boolean +function IntegerModN:le(b) + return self.element <= b.element +end + +--- @return IntegerModN +function IntegerModN:zero() + if not self or not self.modulus then + return Integer.zero() + end + return IntegerModN(Integer.zero(), self.modulus) +end + +--- @return IntegerModN +function IntegerModN:one() + if not self or not self.modulus then + return Integer.one() + end + return IntegerModN(Integer.one(), self.modulus) +end + +--- @return string +function IntegerModN:tolatex(mod) + mod = mod or false + if mod then + return self.element:tolatex() .. "\\bmod{" .. self.modulus:tolatex() .. "}" + else + return self.element:tolatex() + end +end +----------------- +-- Inheritance -- +----------------- + +__IntegerModN.__index = Field +__IntegerModN.__call = IntegerModN.new +IntegerModN = setmetatable(IntegerModN, __IntegerModN)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/logarithm.lua b/macros/luatex/latex/luacas/tex/algebra/logarithm.lua new file mode 100644 index 0000000000..07a9db84d1 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/logarithm.lua @@ -0,0 +1,186 @@ +--- @class Logarithm +--- An expression for the logarithm of an expression with respect to another. +--- Currently, logarithms are not being evaluated since we are just doing symbolic computation. +--- @field base Expression +--- @field expression Expression +Logarithm = {} +__Logarithm = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new logarithm expression with the given symbol and expression. +--- @param base Expression +--- @param expression Expression +--- @ +function Logarithm:new(base, expression) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.base = Copy(base) + o.expression = Copy(expression) + + __o.__index = Logarithm + __o.__tostring = function(a) + return 'log(' .. tostring(base) .. ', ' .. tostring(expression) .. ')' + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not b:type() == Logarithm then + return false + end + return a.base == b.base and a.expression == b.expression + end + o = setmetatable(o, __o) + + return o +end + +--- @return Expression +function Logarithm:evaluate() + + if (self.base:isconstant() and (self.base <= Integer.zero() or self.base == Integer.one())) or + (self.expression:isconstant() and self.expression <= Integer.zero()) then + error("Arithmetic error: division by zero") + end + + if not self.base:isconstant() or not self.expression:isconstant() then + return self + end + + local power = Integer.one() + local base = self.base + if base:type() == Integer then + local pp, b, p = base:isperfectpower() + if pp then + base = b + power = p + end + end + + if base:type() == Rational then + local ppn, bn, pn = base.numerator:isperfectpower() + local ppd, bd, pd = base.denominator:isperfectpower() + if base.numerator == Integer.one() then + ppn = true + bn = Integer.one() + pn = pd + end + if ppn and ppd and pn == pd then + base = bn / bd + power = pn + end + end + + local result = Integer.one() + local expression = self.expression + local sign = Integer.one() + if base < Integer.one() then + base = Integer.one() / base + sign = -sign + end + + + local current = base + while current < expression do + current = current * base + result = result + Integer.one() + end + if current == expression then + return sign * result / power + else + while current > expression do + current = current / base + result = result - Integer.one() + end + if current == expression then + return sign * result / power + end + end + + return self +end + +--- @return Expression +function Logarithm:autosimplify() + + local base = self.base:autosimplify() + local expression = self.expression:autosimplify() + + local evaluated = Logarithm(base, expression):evaluate() + if evaluated:type() ~= Logarithm then + return evaluated + end + + -- Uses the property that log(b, 1) = 0 + if expression == Integer.one() then + return Integer.zero() + end + + -- Uses the property that log(b, b) = 1 + if expression == base then + return Integer.one() + end + + -- Uses the propery that log(b, x^y) = y * log(b, x) + if expression.operation == BinaryOperation.POW then + return BinaryOperation.MULEXP({expression.expressions[2], Logarithm(base, expression.expressions[1])}):autosimplify() + end + + if expression:type() == Rational and expression.numerator == Integer.one() then + return (-Logarithm(base, expression.denominator)):autosimplify() + end + + -- Our expression cannot be simplified + return Logarithm(base, expression) +end + +--- @return Expression +function Logarithm:expand() + return Logarithm(self.base:expand(), self.expression:expand()):autosimplify() +end + +--- @return table<number, Expression> +function Logarithm:subexpressions() + return {self.base, self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return Logarithm +function Logarithm:setsubexpressions(subexpressions) + return Logarithm(subexpressions[1], subexpressions[2]) +end + +--- @param other Expression +--- @return boolean +function Logarithm:order(other) + return FunctionExpression("log", {self.base, self.expression}):order(other) +end + +--- @return string +function Logarithm:tolatex() + if self.base == E then + return '\\ln\\mathopen{}\\left(' .. self.expression:tolatex() .. '\\right)' + end + return '\\log_' .. self.base:tolatex() .. '\\mathopen{}\\left(' .. self.expression:tolatex() .. '\\right)' +end + +----------------- +-- Inheritance -- +----------------- +__Logarithm.__index = CompoundExpression +__Logarithm.__call = Logarithm.new +Logarithm = setmetatable(Logarithm, __Logarithm) + +---------------------- +-- Static constants -- +---------------------- + +LOG = function(base, expression) + return Logarithm(base, expression) +end + +LN = function(expression) + return Logarithm(E, expression) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua new file mode 100644 index 0000000000..568c21c921 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua @@ -0,0 +1,860 @@ +--- @class PolynomialRing +--- Represents an element of a polynomial ring. +--- @field coefficients table<number, Ring> +--- @field symbol SymbolExpression +--- @field ring RingIdentifier +PolynomialRing = {} +__PolynomialRing = {} + +-- Metatable for ring objects. +local __obj = {__index = PolynomialRing, __eq = function(a, b) + return a["ring"] == b["ring"] and + (a["child"] == b["child"] or a["child"] == nil or b["child"] == nil) and + (a["symbol"] == b["symbol"] or a["child"] == nil or b["child"] == nil) +end, __tostring = function(a) + if a.child and a.symbol then return tostring(a.child) .. "[" .. a.symbol .. "]" else return "(Generic Polynomial Ring)" end +end} + +-------------------------- +-- Static functionality -- +-------------------------- + +--- Creates a new ring with the given symbol and child ring. +--- @param symbol SymbolExpression +--- @param child RingIdentifier +--- @return RingIdentifier +function PolynomialRing.makering(symbol, child) + local t = {ring = PolynomialRing} + t.symbol = symbol + t.child = child + t = setmetatable(t, __obj) + return t +end + +-- Shorthand constructor for a polynomial ring with integer or integer mod ring coefficients. +function PolynomialRing.R(symbol, modulus) + if modulus then + return PolynomialRing.makering(symbol, IntegerModN.makering(modulus)) + end + return PolynomialRing.makering(symbol, Integer.getring()) +end + +--- Returns the GCD of two polynomials in a ring, assuming both rings are euclidean domains. +--- @param a PolynomialRing +--- @param b PolynomialRing +--- @return PolynomialRing +function PolynomialRing.gcd(a, b) + if a.symbol ~= b.symbol then + error("Cannot take the gcd of two polynomials with different symbols") + end + while b ~= Integer.zero() do + a, b = b, a % b + end + return a // a:lc() +end + +-- Returns the GCD of two polynomials in a ring, assuming both rings are euclidean domains. +-- Also returns bezouts coefficients via extended gcd. +--- @param a PolynomialRing +--- @param b PolynomialRing +--- @return PolynomialRing, PolynomialRing, PolynomialRing +function PolynomialRing.extendedgcd(a, b) + local oldr, r = a, b + local olds, s = Integer.one(), Integer.zero() + local oldt, t = Integer.zero(), Integer.one() + while r ~= Integer.zero() do + local q = oldr // r + oldr, r = r, oldr - q*r + olds, s = s, olds - q*s + oldt, t = t, oldt - q*t + end + return oldr // oldr:lc(), olds // oldr:lc(), oldt // oldr:lc() +end + +-- Returns the resultant of two polynomials in the same ring, whose coefficients are all part of a field. +--- @param a PolynomialRing +--- @param b PolynomialRing +--- @return Field +function PolynomialRing.resultant(a, b) + + if a.ring == PolynomialRing.getring() or b.ring == PolynomialRing.getring() then + return PolynomialRing.resultantmulti(a, b) + end + + local m, n = a.degree, b.degree + if n == Integer.zero() then + return b.coefficients[0]^m + end + + local r = a % b + if r == Integer.zero() then + return r.coefficients[0] + end + + local s = r.degree + local l = b:lc() + + return Integer(-1)^(m*n) * l^(m-s) * PolynomialRing.resultant(b, r) +end + +-- Returns the resultant of two polynomials in the same ring, whose coefficients are not part of a field. +--- @param a PolynomialRing +--- @param b PolynomialRing +--- @return Ring +function PolynomialRing.resultantmulti(a, b) + local m, n = a.degree, b.degree + + if m < n then + return Integer(-1) ^ (m * n) * PolynomialRing.resultantmulti(b, a) + end + if n == Integer.zero() then + return b.coefficients[0]^m + end + + local delta = m - n + Integer(1) + local _ , r = PolynomialRing.pseudodivide(a, b) + if r == Integer.zero() then + return r.coefficients[0] + end + + local s = r.degree + local w = Integer(-1)^(m*n) * PolynomialRing.resultant(b, r) + local l = b:lc() + local k = delta * n - m + s + local f = l ^ k + return w // f +end + +-- Given two polynomials a and b, returns a list of the remainders generated by the monic Euclidean algorithm. +--- @param a PolynomialRing +--- @param b PolynomialRing +--- @return table<number, Ring> +function PolynomialRing.monicgcdremainders(a, b) + if a.symbol ~= b.symbol then + error("Cannot take the gcd of two polynomials with different symbols") + end + + local remainders = {a / a:lc(), b / b:lc()} + while true do + local q = remainders[#remainders - 1] // remainders[#remainders] + local c = remainders[#remainders - 1] - q*remainders[#remainders] + if c ~= Integer.zero() then + remainders[#remainders+1] = c/c:lc() + else + break + end + end + + return remainders +end + +-- Returns the partial fraction decomposition of the rational function g/f +-- given g, f, and some (not nessecarily irreducible) factorization of f. +-- If the factorization is omitted, the irreducible factorization is used. +-- The degree of g must be less than the degree of f. +--- @param g PolynomialRing +--- @param f PolynomialRing +--- @param ffactors Expression +--- @return Expression +function PolynomialRing.partialfractions(g, f, ffactors) + + if g.degree >= f.degree then + error("Argument Error: The degree of g must be less than the degree of f.") + end + + -- Converts f to a monic polynomial. + g = g * f:lc() + f = f / f:lc() + + ffactors = ffactors or f:factor() + + local expansionterms = {} + + for _, factor in ipairs(ffactors.expressions) do + local k + local m + if factor.getring and factor:getring() == PolynomialRing:getring() then + m = factor + k = Integer.one() + elseif not factor:isconstant() then + m = factor.expressions[1] + k = factor.expressions[2] + end + + if not factor:isconstant() then + -- Uses Chinese Remainder Theorem for each factor to determine the numerator of the term in the decomposition + local mk = m^k + local v = g % mk + local _, minv, _ = PolynomialRing.extendedgcd(f // mk, mk) + local c = v*minv % mk + + + if k == Integer.one() then + expansionterms[#expansionterms+1] = BinaryOperation.ADDEXP({BinaryOperation.DIVEXP({c, BinaryOperation.POWEXP({m, Integer.one()})})}) + else + -- Uses the p-adic expansion of c to split terms with repeated roots. + local q = c + local r + local innerterms = {} + for i = k:asnumber(), 1, -1 do + q, r = q:divremainder(m) + innerterms[#innerterms+1] = BinaryOperation.DIVEXP({r, BinaryOperation.POWEXP({m, Integer(i)})}) + end + expansionterms[#expansionterms+1] = BinaryOperation.ADDEXP(innerterms) + end + end + end + + return BinaryOperation.ADDEXP(expansionterms) + +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- So we don't have to copy the Euclidean operations each time +local __o = Copy(__EuclideanOperations) +__o.__index = PolynomialRing +__o.__tostring = function(a) + local out = "" + local loc = a.degree:asnumber() + while loc >= 0 do + if a.ring == PolynomialRing.getring() or (a.ring == Rational.getring() and a.ring.symbol) then + out = out .. "(" .. tostring(a.coefficients[loc]) .. ")" .. a.symbol .. "^" .. tostring(math.floor(loc)) .. "+" + else + out = out .. tostring(a.coefficients[loc]) .. a.symbol .. "^" .. tostring(math.floor(loc)) .. "+" + end + loc = loc - 1 + end + return string.sub(out, 1, string.len(out) - 1) +end +__o.__div = function(a, b) + if not b.getring then + return BinaryOperation.DIVEXP({a, b}) + end + if Ring.resultantring(a.ring, b:getring()) ~= Ring.resultantring(a:getring(), b:getring()) then + return a:div(b:inring(Ring.resultantring(a:getring(), b:getring()))) + end + if b.ring and b:getring() == Rational:getring() and a.symbol == b.ring.symbol then + return a:inring(Ring.resultantring(a:getring(), b:getring())):div(b) + end + if a:getring() == b:getring() then + return Rational(a, b, true) + end + -- TODO: Fix this for arbitrary depth + if a:getring() == PolynomialRing:getring() and b:getring() == PolynomialRing:getring() and a.symbol == b.symbol then + local oring = Ring.resultantring(a:getring(), b:getring()) + return Rational(a:inring(oring), b:inring(oring), true) + end + return BinaryOperation.DIVEXP({a, b}) +end + +function PolynomialRing:tolatex() + local out = '' + local loc = self.degree:asnumber() + if loc == 0 then + return self.coefficients[loc]:tolatex() + end + if self.ring == Rational.getring() or self.ring == Integer.getring() or self.ring == IntegerModN.getring() then + if self.coefficients[loc] ~= Integer.one() then + out = out .. self.coefficients[loc]:tolatex() .. self.symbol + else + out = out .. self.symbol + end + if loc ~=1 then + out = out .. "^{" .. loc .. "}" + end + loc = loc -1 + while loc >=0 do + local coeff = self.coefficients[loc] + if coeff == Integer.one() then + if loc == 0 then + out = out .. "+" .. coeff:tolatex() + goto skip + else + out = out .. "+" + goto continue + end + end + if coeff == Integer(-1) then + if loc == 0 then + out = out .. "-" .. coeff:neg():tolatex() + goto skip + else + out = out .. "-" + goto continue + end + end + if coeff < Integer.zero() then + out = out .. "-" .. coeff:neg():tolatex() + end + if coeff == Integer.zero() then + goto skip + end + if coeff > Integer.zero() then + out = out .. "+" .. coeff:tolatex() + end + ::continue:: + if loc > 1 then + out = out .. self.symbol .. "^{" .. loc .. "}" + end + if loc == 1 then + out = out .. self.symbol + end + ::skip:: + loc = loc-1 + end + else + while loc >=0 do + if loc >=1 then + out = out .. self.coefficients[loc]:tolatex() .. self.symbol .. "^{" .. loc .. "} + " + else + out = out .. self.coefficients[loc]:tolatex() .. self.symbol .. "^{" .. loc .. "}" + end + loc = loc-1 + end + end + return out +end + +function PolynomialRing:isatomic() + --if self.degree >= Integer.one() then + -- return false + --else + return false + --end +end +--test + +-- Creates a new polynomial ring given an array of coefficients and a symbol +function PolynomialRing:new(coefficients, symbol, degree) + local o = {} + o = setmetatable(o, __o) + + if type(coefficients) ~= "table" then + error("Sent parameter of wrong type: Coefficients must be in an array") + end + o.coefficients = {} + o.degree = degree or Integer(-1) + + if type(symbol) ~= "string" and not symbol.symbol then + error("Symbol must be a string") + end + o.symbol = symbol.symbol or symbol + + -- Determines what ring the polynomial ring should have as its child + for index, coefficient in pairs(coefficients) do + if type(index) ~= "number" then + error("Sent parameter of wrong type: Coefficients must be in an array") + end + if not coefficient.getring then + error("Sent parameter of wrong type: Coefficients must be elements of a ring") + end + if not o.ring then + o.ring = coefficient:getring() + else + local newring = coefficient:getring() + local combinedring = Ring.resultantring(o.ring, newring) + if combinedring == newring then + o.ring = newring + elseif not o.ring == combinedring then + error("Sent parameter of wrong type: Coefficients must all be part of the same ring") + end + end + end + + if not coefficients[0] then + -- Constructs the coefficients when a new polynomial is instantiated as an array + for index, coefficient in ipairs(coefficients) do + o.coefficients[index - 1] = coefficient + o.degree = o.degree + Integer.one() + end + else + -- Constructs the coefficients from an existing polynomial of coefficients + local loc = o.degree:asnumber() + while loc > 0 do + if not coefficients[loc] or coefficients[loc] == coefficients[loc]:zero() then + o.degree = o.degree - Integer.one() + else + break + end + loc = loc - 1 + end + + while loc >= 0 do + o.coefficients[loc] = coefficients[loc] + loc = loc - 1 + end + end + + -- Each value of the polynomial greater than its degree is implicitly zero + o.coefficients = setmetatable(o.coefficients, {__index = function (table, key) + return o:zeroc() + end}) + return o +end + +-- Returns the ring this object is an element of +function PolynomialRing:getring() + local t = {ring = PolynomialRing} + if self then + t.child = self.ring + t.symbol = self.symbol + end + t = setmetatable(t, __obj) + return t +end + +-- Explicitly converts this element to an element of another ring +function PolynomialRing:inring(ring) + + -- Faster equality check + if ring == self:getring() then + return self + end + + if ring == Rational:getring() and ring.symbol then + return Rational(self:inring(ring.child), self:inring(ring.child):one(), true) + end + + if ring.symbol == self.symbol then + local out = {} + for i = 0, self.degree:asnumber() do + out[i + 1] = self.coefficients[i]:inring(ring.child) + end + return PolynomialRing(out, self.symbol) + end + + -- TODO: Allow re-ordering of polynomial rings, so from R[x][y] -> R[y][x] for instance + if ring == PolynomialRing:getring() then + return PolynomialRing({self:inring(ring.child)}, ring.symbol) + end + + error("Unable to convert element to proper ring.") +end + + +-- Returns whether the ring is commutative +function PolynomialRing:iscommutative() + return true +end + +function PolynomialRing:add(b) + local larger + + if self.degree > b.degree then + larger = self + else + larger = b + end + + local new = {} + local loc = 0 + while loc <= larger.degree:asnumber() do + new[loc] = self.coefficients[loc] + b.coefficients[loc] + loc = loc + 1 + end + + return PolynomialRing(new, self.symbol, larger.degree) +end + +function PolynomialRing:neg() + local new = {} + local loc = 0 + while loc <= self.degree:asnumber() do + new[loc] = -self.coefficients[loc] + loc = loc + 1 + end + return PolynomialRing(new, self.symbol, self.degree) +end + +function PolynomialRing:mul(b) + -- Grade-school multiplication is actually faster up to a very large polynomial size due to Lua's overhead. + local new = {} + + local sd = self.degree:asnumber() + local bd = b.degree:asnumber() + + for i = 0, sd+bd do + new[i] = self:zeroc() + for j = math.max(0, i-bd), math.min(sd, i) do + new[i] = new[i] + self.coefficients[j]*b.coefficients[i-j] + end + end + return PolynomialRing(new, self.symbol, self.degree + b.degree) + -- return PolynomialRing(PolynomialRing.mul_rec(self.coefficients, b.coefficients), self.symbol, self.degree + b.degree) +end + +-- Performs Karatsuba multiplication without constructing new polynomials recursively +function PolynomialRing.mul_rec(a, b) + if #a==0 and #b==0 then + return {[0]=a[0] * b[0], [1]=Integer.zero()} + end + + local k = Integer.ceillog(Integer.max(Integer(#a), Integer(#b)) + Integer.one(), Integer(2)) + local n = Integer(2) ^ k + local m = n / Integer(2) + local nn = n:asnumber() + local mn = m:asnumber() + + local a0, a1, b0, b1 = {}, {}, {}, {} + + for e = 0, mn - 1 do + a0[e] = a[e] or Integer.zero() + a1[e] = a[e + mn] or Integer.zero() + b0[e] = b[e] or Integer.zero() + b1[e] = b[e + mn] or Integer.zero() + end + + local p1 = PolynomialRing.mul_rec(a1, b1) + local p2a = Copy(a0) + local p2b = Copy(b0) + for e = 0, mn - 1 do + p2a[e] = p2a[e] + a1[e] + p2b[e] = p2b[e] + b1[e] + end + local p2 = PolynomialRing.mul_rec(p2a, p2b) + local p3 = PolynomialRing.mul_rec(a0, b0) + local r = {} + for e = 0, mn - 1 do + p2[e] = p2[e] - p1[e] - p3[e] + r[e] = p3[e] + r[e + mn] = p2[e] + r[e + nn] = p1[e] + end + for e = mn, nn - 1 do + p2[e] = p2[e] - p1[e] - p3[e] + r[e] = r[e] + p3[e] + r[e + mn] = r[e + mn] + p2[e] + r[e + nn] = p1[e] + end + + return r +end + +-- Uses synthetic division. +function PolynomialRing:divremainder(b) + local n, m = self.degree:asnumber(), b.degree:asnumber() + + if m > n then + return self:zero(), self + end + + local o = Copy(self.coefficients) + local lc = b:lc() + for i = n, m, -1 do + o[i] = o[i] / lc + + if o[i] ~= self:zeroc() then + for j = 1, m do + o[i-j] = o[i-j] - b.coefficients[m - j] * o[i] + end + end + end + + local q = {} + local r = {} + for i = 0, m-1 do + r[i] = o[i] + end + + r[0] = r[0] or self:zeroc() + + for i = m, #o do + q[i - m] = o[i] + end + + return PolynomialRing(q, self.symbol, self.degree), PolynomialRing(r, self.symbol, Integer.max(Integer.zero(), b.degree-Integer.one())) +end + +-- Performs polynomial pseudodivision of this polynomial by another in the same ring, +-- and returns both the pseudoquotient and pseudoremainder. +-- In the case where both coefficients are fields, this is equivalent to division with remainder. +function PolynomialRing:pseudodivide(b) + + local p = self:zero() + local s = self + local m = s.degree + local n = b.degree + local delta = Integer.max(m - n + Integer.one(), Integer.zero()) + + local lcb = b:lc() + local sigma = Integer.zero() + + while m >= n and s ~= Integer.zero() do + local lcs = s:lc() + p = p * lcb + self:one():multiplyDegree((m-n):asnumber()) * lcs + s = s * lcb - b * self:one():multiplyDegree((m-n):asnumber()) * lcs + sigma = sigma + Integer.one() + m = s.degree + end + + if delta - sigma == Integer.zero() then + return p,s + else + return lcb^(delta - sigma) * p, lcb^(delta - sigma) * s + end +end + +-- Polynomial rings are never fields, but when dividing by a polynomial by a constant we may want to use / instead of // +function PolynomialRing:div(b) + return self:divremainder(b) +end + +function PolynomialRing:zero() + return self.coefficients[0]:zero():inring(self:getring()) +end + +function PolynomialRing:zeroc() + return self.coefficients[0]:zero() +end + +function PolynomialRing:one() + return self.coefficients[0]:one():inring(self:getring()) +end + +function PolynomialRing:onec() + return self.coefficients[0]:one() +end + +function PolynomialRing:eq(b) + for i=0,math.max(self.degree:asnumber(), b.degree:asnumber()) do + if self.coefficients[i] ~= b.coefficients[i] then + return false + end + end + return true +end + +-- Returns the leading coefficient of this polynomial +function PolynomialRing:lc() + return self.coefficients[self.degree:asnumber()] +end + +--- @return boolean +function PolynomialRing:isconstant() + return false +end + +-- This expression is free of a symbol if and only if the symbol is not the symbol used to create the ring. +function PolynomialRing:freeof(symbol) + return symbol.symbol ~= self.symbol +end + +-- Replaces each expression in the map with its value. +function PolynomialRing:substitute(map) + return self:tocompoundexpression():substitute(map) +end + +-- Expands a polynomial expression. Polynomials are already in expanded form, so we just need to autosimplify. +function PolynomialRing:expand() + return self:tocompoundexpression():autosimplify() +end + +function PolynomialRing:autosimplify() + return self:tocompoundexpression():autosimplify() +end + +-- Transforms from array format to an expression format. +function PolynomialRing:tocompoundexpression() + local terms = {} + for exponent, coefficient in pairs(self.coefficients) do + terms[exponent + 1] = BinaryOperation(BinaryOperation.MUL, {coefficient:tocompoundexpression(), + BinaryOperation(BinaryOperation.POW, {SymbolExpression(self.symbol), Integer(exponent)})}) + end + return BinaryOperation(BinaryOperation.ADD, terms) +end + +-- Uses Horner's rule to evaluate a polynomial at a point +function PolynomialRing:evaluateat(x) + local out = self:zeroc() + for i = self.degree:asnumber(), 1, -1 do + out = out + self.coefficients[i] + out = out * x + end + return out + self.coefficients[0] +end + +-- Multiplies this polynomial by x^n +function PolynomialRing:multiplyDegree(n) + local new = {} + for e = 0, n-1 do + new[e] = self:zeroc() + end + local loc = n + while loc <= self.degree:asnumber() + n do + new[loc] = self.coefficients[loc - n] + loc = loc + 1 + end + return PolynomialRing(new, self.symbol, self.degree + Integer(n)) +end + +-- Returns the formal derivative of this polynomial +function PolynomialRing:derivative() + if self.degree == Integer.zero() then + return PolynomialRing({self:zeroc()}, self.symbol, Integer(-1)) + end + local new = {} + for e = 1, self.degree:asnumber() do + new[e - 1] = Integer(e) * self.coefficients[e] + end + return PolynomialRing(new, self.symbol, self.degree - Integer.one()) +end + +-- Returns the square-free factorization of a polynomial +function PolynomialRing:squarefreefactorization() + local terms + if self.ring == Rational.getring() or self.ring == Integer.getring() then + terms = self:rationalsquarefreefactorization() + elseif self.ring == IntegerModN.getring() then + if not self.ring.modulus:isprime() then + error("Cannot compute a square-free factorization of a polynomial ring contructed from a ring that is not a field.") + end + terms = self:modularsquarefreefactorization() + end + + local expressions = {self:lc()} + local j = 1 + for index, term in ipairs(terms) do + if term.degree ~= Integer.zero() or term.coefficients[0] ~= Integer.one() then + j = j + 1 + expressions[j] = BinaryOperation.POWEXP({term, Integer(index)}) + end + end + + return BinaryOperation.MULEXP(expressions) +end + +-- Factors a polynomial into irreducible terms +function PolynomialRing:factor() + -- Square-free factorization over an integral domain (so a polynomial ring constructed from a field) + local squarefree = self:squarefreefactorization() + local squarefreeterms = {} + local result = {squarefree.expressions[1]} + for i, expression in ipairs(squarefree.expressions) do + if i > 1 then + -- Converts square-free polynomials with rational coefficients to integer coefficients so Rational Roots / Zassenhaus can factor them + if expression.expressions[1].ring == Rational.getring() then + local factor, integerpoly = expression.expressions[1]:rationaltointeger() + result[1] = result[1] * factor ^ expression.expressions[2] + squarefreeterms[i - 1] = integerpoly + else + squarefreeterms[i - 1] = expression.expressions[1] + end + end + end + + for i, expression in ipairs(squarefreeterms) do + local terms + if expression.ring == Integer.getring() then + -- Factoring over the integers first uses the rational roots test to factor out monomials (for efficiency purposes) + local remaining, factors = expression:rationalroots() + terms = factors + -- Then applies the Zassenhaus algorithm if there entire polynomial has not been factored into monomials + if remaining ~= Integer.one() then + remaining = remaining:zassenhausfactor() + for _, exp in ipairs(remaining) do + terms[#terms+1] = exp + end + end + end + if expression.ring == IntegerModN.getring() then + -- Berlekamp factorization is used for rings with integers mod a prime as coefficients + terms = expression:berlekampfactor() + end + for _, factor in ipairs(terms) do + result[#result+1] = BinaryOperation.POWEXP({factor, squarefree.expressions[i + 1].expressions[2]}) + end + end + return BinaryOperation.MULEXP(result) +end + +-- Uses the Rational Root test to factor out monomials of a square-free polynomial. +function PolynomialRing:rationalroots() + local remaining = self + local roots = {} + if self.coefficients[0] == Integer.zero() then + roots[1] = PolynomialRing({Integer.zero(), Integer.one()}, self.symbol) + remaining = remaining // roots[1] + end + -- This can be slower than Zassenhaus if the digits are large enough, since factoring integers is slow + -- if self.coefficients[0] > Integer(Integer.DIGITSIZE - 1) or self:lc() > Integer(Integer.DIGITSIZE - 1) then + -- return remaining, roots + -- end + while remaining ~= Integer.one() do + :: nextfactor :: + local a = remaining.coefficients[0] + local b = remaining:lc() + local afactors = a:divisors() + local bfactors = b:divisors() + for _, af in ipairs(afactors) do + for _, bf in ipairs(bfactors) do + local testroot = Rational(af, bf, true) + if remaining:evaluateat(testroot) == Integer.zero() then + roots[#roots+1] = PolynomialRing({-testroot.numerator, testroot.denominator}, self.symbol) + remaining = remaining // roots[#roots] + goto nextfactor + end + if remaining:evaluateat(-testroot) == Integer.zero() then + roots[#roots+1] = PolynomialRing({testroot.numerator, testroot.denominator}, self.symbol) + remaining = remaining // roots[#roots] + goto nextfactor + end + end + end + break + end + + return remaining, roots +end + +-- Returns a list of roots of the polynomial, simplified up to cubics. +function PolynomialRing:roots() + local roots = {} + local factorization = self:factor() + + for i, factor in ipairs(factorization.expressions) do + if i > 1 then + local decomp = factor.expressions[1]:decompose() + for _, poly in ipairs(decomp) do + if poly.degree > Integer(3) then + table.insert(roots,RootExpression(factor.expressions[1])) + goto nextfactor + end + end + local factorroots = RootExpression(decomp[#decomp]):autosimplify() + if factorroots == true then + return true + end + if factorroots == false then + goto nextfactor + end + local replaceroots = {} + for j = #decomp - 1,1,-1 do + for _, root in ipairs(factorroots) do + local temp = RootExpression(decomp[j]):autosimplify(root) + if temp == true then + return true + end + if factorroots == false then + goto nextfactor + end + replaceroots = JoinArrays(replaceroots, temp) + end + factorroots = replaceroots + end + roots = JoinArrays(roots, factorroots) + end + end + ::nextfactor:: + return roots +end + +----------------- +-- Inheritance -- +----------------- + +__PolynomialRing.__index = Ring +__PolynomialRing.__call = PolynomialRing.new +PolynomialRing = setmetatable(PolynomialRing, __PolynomialRing)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua new file mode 100644 index 0000000000..feabb61a3f --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua @@ -0,0 +1,187 @@ +-- Methods related to the Berlekamp factoring algorithm. + +-- Square-free factorization in the modular field Zp. +function PolynomialRing:modularsquarefreefactorization() + local monic = self / self:lc() + local terms = {} + terms[0] = PolynomialRing.gcd(monic, monic:derivative()) + local b = monic // terms[0] + local c = monic:derivative() // terms[0] + local d = c - b:derivative() + local i = 1 + while b ~= Integer.one() do + terms[i] = PolynomialRing.gcd(b, d) + b, c = b // terms[i], d // terms[i] + i = i + 1 + d = c - b:derivative() + end + + if not (terms[i-1]:derivative().degree == Integer.zero() and terms[i-1]:derivative().coefficients[0] == Integer.zero()) then + return terms + end + + local recursiveterms = terms[i-1]:collapseterms(self.ring.modulus):modularsquarefreefactorization() + for k, poly in ipairs(recursiveterms) do + recursiveterms[k] = poly:expandterms(self.ring.modulus) + end + return JoinArrays(terms, recursiveterms) +end + +-- Returns a new polnomial consisting of every nth term of the old one - helper method for square-free factorization +function PolynomialRing:collapseterms(n) + local new = {} + local loc = 0 + local i = 0 + local nn = n:asnumber() + while loc <= self.degree:asnumber() do + new[i] = self.coefficients[loc] + loc = loc + nn + i = i + 1 + end + + return PolynomialRing(new, self.symbol, self.degree // n) +end + +-- Returns a new polnomial consisting of every nth term of the old one - helper method for square-free factorization +function PolynomialRing:expandterms(n) + local new = {} + local loc = 0 + local i = 0 + local nn = n:asnumber() + while i <= self.degree:asnumber() do + new[loc] = self.coefficients[i] + for j = 1, nn do + new[loc + j] = IntegerModN(Integer.zero(), n) + end + loc = loc + nn + i = i + 1 + end + + return PolynomialRing(new, self.symbol, self.degree * n) +end + +-- Uses Berlekamp's Algorithm to factor polynomials in mod p +function PolynomialRing:berlekampfactor() + if self.degree == 0 or self.degree == 1 then + return {self} + end + + local R = self:RMatrix() + local S = self:auxillarybasis(R) + if #S == 1 then + return {self} + end + return self:findfactors(S) +end + +-- Gets the R Matrix for Berlekamp factorization +function PolynomialRing:RMatrix() + local R = {} + for i = 1, self.degree:asnumber() do + R[i] = {} + end + for i = 0, self.degree:asnumber()-1 do + local remainder = PolynomialRing({IntegerModN(Integer.one(), self.ring.modulus)}, self.symbol):multiplyDegree(self.ring.modulus:asnumber()*i) % self + for j = 0, self.degree:asnumber()-1 do + R[j + 1][i + 1] = remainder.coefficients[j] + if j == i then + R[j + 1][i + 1] = R[j + 1][i + 1] - IntegerModN(Integer.one(), self.ring.modulus) + end + end + end + return R +end + +-- Creates an auxillary basis using the R matrix +function PolynomialRing:auxillarybasis(R) + local P = {} + local n = self.degree:asnumber() + for i = 1, n do + P[i] = 0 + end + S = {} + local q = 1 + for j = 1, n do + local i = 1 + local pivotfound = false + while not pivotfound and i <= n do + if R[i][j] ~= self:zeroc() and P[i] == 0 then + pivotfound = true + else + i = i + 1 + end + end + if pivotfound then + P[i] = j + local a = R[i][j]:inv() + for l = 1, n do + R[i][l] = a * R[i][l] + end + for k = 1, n do + if k ~= i then + local f = R[k][j] + for l = 1, n do + R[k][l] = R[k][l] - f*R[i][l] + end + end + end + else + local s = {} + s[j] = self:onec() + for l = 1, j - 1 do + local e = 0 + i = 1 + while e == 0 and i <= n do + if l == P[i] then + e = i + else + i = i + 1 + end + end + if e > 0 then + local c = -R[e][j] + s[l] = c + else + s[l] = self:zeroc() + end + end + S[#S+1] = PolynomialRing(s, self.symbol) + end + end + return S +end + +-- Uses the auxilary basis to find the irreirrducible factors of the polynomial. +function PolynomialRing:findfactors(S) + local r = #S + local p = self.ring.modulus + local factors = {self} + for k = 2,r do + local b = S[k] + local old_factors = Copy(factors) + for i = 1,#old_factors do + local w = old_factors[i] + local j = 0 + while j <= p:asnumber() - 1 do + local g = PolynomialRing.gcd(b-IntegerModN(Integer(j), p), w) + if g == Integer.one() then + j = j + 1 + elseif g == w then + j = p:asnumber() + else + factors = Remove(factors, w) + local q = w // g + factors[#factors+1] = g + factors[#factors+1] = q + if #factors == r then + return factors + else + j = j + 1 + w = q + end + end + + end + end + end +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua new file mode 100644 index 0000000000..84f2af1efc --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua @@ -0,0 +1,93 @@ +-- Methods related to polynomial decomposition. + +-- Returns a list of polynomials that form a complete decomposition of a polynomial. +function PolynomialRing:decompose() + local U = self - self.coefficients[0] + local S = U:divisors() + local decomposition = {} + local C = PolynomialRing({Integer.zero(), Integer.one()}, self.symbol) + local finalcomponent + + while S[1] do + local w = S[1] + for _, poly in ipairs(S) do + if poly.degree < w.degree then + w = poly + end + end + S = Remove(S, w) + if C.degree < w.degree and w.degree < self.degree and self.degree % w.degree == Integer.zero() then + local g = w:polyexpand(C, self.symbol) + local R = self:polyexpand(w, self.symbol) + if g.degree == Integer.zero() and R.degree == Integer.zero() then + g.symbol = self.symbol + decomposition[#decomposition+1] = g.coefficients[0] + decomposition[#decomposition].symbol = self.symbol + C = w + finalcomponent = R.coefficients[0] + end + end + end + + if not decomposition[1] then + return {self} + end + + finalcomponent.symbol = self.symbol + decomposition[#decomposition+1] = finalcomponent + return decomposition +end + +-- Returns a list of all monic divisors of positive degree of the polynomial, assuming the polynomial ring is a Euclidean Domain. +function PolynomialRing:divisors() + local factors = self:factor() + -- Converts each factor to a monic factor (we don't need to worry updating the constant term) + for i, factor in ipairs(factors.expressions) do + if i > 1 then + factor.expressions[1] = factor.expressions[1] / factor.expressions[1]:lc() + end + end + + local terms = {} + for i, _ in ipairs(factors.expressions) do + if i > 1 then + terms[i] = Integer.zero() + end + end + + local divisors = {} + local divisor = PolynomialRing({self:onec()}, self.symbol) + while true do + for i, factor in ipairs(factors.expressions) do + if i > 1 then + local base = factor.expressions[1] + local power = factor.expressions[2] + if terms[i] < power then + terms[i] = terms[i] + Integer.one() + divisor = divisor * base + break + else + terms[i] = Integer.zero() + divisor = divisor // (base ^ power) + end + end + end + if divisor == Integer.one() then + break + end + divisors[#divisors+1] = divisor + end + + return divisors + +end + +-- Polynomial expansion as a subroutine of decomposition. +function PolynomialRing:polyexpand(v, x) + local u = self + if u == Integer.zero() then + return Integer.zero() + end + local q,r = u:divremainder(v) + return PolynomialRing({PolynomialRing({Integer.zero(), Integer.one()}, "_")}, x) * q:polyexpand(v, x) + r +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua new file mode 100644 index 0000000000..be19298e64 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua @@ -0,0 +1,220 @@ +-- Methods related to the Zassenhaus factorization algorithm. + + +-- Square-free factorization in the rational field. +function PolynomialRing:rationalsquarefreefactorization(keeplc) + local monic = self / self:lc() + local terms = {} + terms[0] = PolynomialRing.gcd(monic, monic:derivative()) + local b = monic // terms[0] + local c = monic:derivative() // terms[0] + local d = c - b:derivative() + local i = 1 + while b.degree ~= Integer.zero() or b.coefficients[0] ~= Integer.one() do + terms[i] = PolynomialRing.gcd(b, d) + b, c = b // terms[i], d // terms[i] + i = i + 1 + d = c - b:derivative() + end + if keeplc and terms[1] then + terms[1] = terms[1] * self:lc() + end + return terms +end + +-- Factors the largest possible constant out of a polynomial whos underlying ring is a Euclidean domain but not a field +function PolynomialRing:factorconstant() + local gcd = Integer.zero() + for i = 0, self.degree:asnumber() do + gcd = self.ring.gcd(gcd, self.coefficients[i]) + end + if gcd == Integer.zero() then + return Integer.one(), self + end + return gcd, self / gcd +end + +-- Converts a polynomial in the rational polynomial ring to the integer polynomial ring +function PolynomialRing:rationaltointeger() + local lcm = Integer.one() + for i = 0, self.degree:asnumber() do + if self.coefficients[i]:getring() == Rational:getring() then + lcm = lcm * self.coefficients[i].denominator / Integer.gcd(lcm, self.coefficients[i].denominator) + end + end + return Integer.one() / lcm, self * lcm +end + +-- Uses Zassenhaus's Algorithm to factor sqaure-free polynomials over the intergers +function PolynomialRing:zassenhausfactor() + + -- Creates a monic polynomial V with related roots + local V = {} + local n = self.degree:asnumber() + local l = self:lc() + for i = 0, n - 1 do + V[i] = l ^ Integer(n - 1 - i) * self.coefficients[i] + end + V[n] = Integer.one() + V = PolynomialRing(V, "y", self.degree) + + -- Performs Berlekamp Factorization in a sutable prime base + local p = V:findprime() + local S = V:inring(PolynomialRing.R("y", p)):berlekampfactor() + + -- If a polynomial is irreducible with coefficients in mod p, it is also irreducible over the integers + if #S == 1 then + return {self} + end + + -- Performs Hensel lifting on the factors mod p + local k = V:findmaxlifts(p) + local W = V:henselift(S, k) + local M = {} + + -- Returns the solutions back to the original from the monic transformation + for i, factor in ipairs(W) do + local w = {} + for j = 0, factor.degree:asnumber() do + w[j] = factor.coefficients[j]:inring(Integer.getring()) * l ^ Integer(j) + end + _, M[i] = PolynomialRing(w, self.symbol, factor.degree):factorconstant() + end + + return M + +end + +-- Finds the smallest prime such that this polynomial with coefficients in mod p is square-free +function PolynomialRing:findprime() + + local smallprimes = {Integer(2), Integer(3), Integer(5), Integer(7), Integer(11), Integer(13), Integer(17), Integer(19), Integer(23), + Integer(29), Integer(31), Integer(37), Integer(41), Integer(43), Integer(47), Integer(53), Integer(59)} + + for _, p in pairs(smallprimes) do + local P = PolynomialRing({IntegerModN(Integer.one(), p)}, self.symbol) + local s = self:inring(P:getring()) + if PolynomialRing.gcd(s, s:derivative()) == P then + return p + end + end + + error("Execution error: No suitable prime found for factoring.") +end + +-- Finds the maximum number of times Hensel Lifting will be applied to raise solutions to the appropriate power +function PolynomialRing:findmaxlifts(p) + local n = self.degree:asnumber() + local h = self.coefficients[0] + for i=0 , n do + if self.coefficients[i] > h then + h = self.coefficients[i] + end + end + + local B = 2^n * math.sqrt(n) * h:asnumber() + return Integer(math.ceil(math.log(2*B, p:asnumber()))) +end + +-- Uses Hensel lifting on the factors of a polynomial S mod p to find them in the integers +function PolynomialRing:henselift(S, k) + local p = S[1].ring.modulus + if k == Integer.one() then + return self:truefactors(S, p, k) + end + G = self:genextendsigma(S) + local V = S + for j = 2, k:asnumber() do + local Vp = V[1]:inring(PolynomialRing.R("y")) + for i = 2, #V do + Vp = Vp * V[i]:inring(PolynomialRing.R("y")) + end + local E = self - Vp:inring(PolynomialRing.R("y")) + if E == Integer.zero() then + return V + end + E = E:inring(PolynomialRing.R("y", p ^ Integer(j))):inring(PolynomialRing.R("y")) + F = E / p ^ (Integer(j) - Integer.one()) + R = self:genextendR(V, G, F) + local Vnew = {} + for i, v in ipairs(V) do + local vnew = v:inring(PolynomialRing.R("y", p ^ Integer(j))) + local rnew = R[i]:inring(PolynomialRing.R("y", p ^ Integer(j))) + Vnew[i] = vnew + (p) ^ (Integer(j) - Integer.one()) * rnew + end + V = Vnew + end + return self:truefactors(V, p, k) +end + +-- Gets a list of sigma polynomials for use in hensel lifting +function PolynomialRing:genextendsigma(S) + local v = S[1] * S[2] + local _, A, B = PolynomialRing.extendedgcd(S[2], S[1]) + local SIGMA = {A, B} + for i, _ in ipairs(S) do + if i >= 3 then + v = v * S[i] + local sum = SIGMA[1] * (v // S[1]) + for j = 2, i-1 do + sum = sum + SIGMA[j] * (v // S[j]) + end + _, A, B = PolynomialRing.extendedgcd(sum, v // S[i]) + for j = 1, i-1 do + SIGMA[j] = SIGMA[j] * A + end + SIGMA[i] = B + end + end + + return SIGMA +end + +-- Gets a list of r polynomials for use in hensel lifting +function PolynomialRing:genextendR(V, G, F) + R = {} + for i, v in ipairs(V) do + local pring = G[1]:getring() + R[i] = F:inring(pring) * G[i] % v:inring(pring) + end + return R +end + +-- Updates factors of the polynomial to the correct ones in the integer ring +function PolynomialRing:truefactors(l, p, k) + local U = self + local L = l + local factors = {} + local m = 1 + while m <= #L / 2 do + local C = Subarrays(L, m) + while #C > 0 do + local t = C[1] + local prod = t[1] + for i = 2, #t do + prod = prod * t[i] + end + local T = prod:inring(PolynomialRing.R("y", p ^ k)):inring(PolynomialRing.R("y")) + -- Convert to symmetric representation - this is the only place it actually matters + for i = 0, T.degree:asnumber() do + if T.coefficients[i] > p ^ k / Integer(2) then + T.coefficients[i] = T.coefficients[i] - p^k + end + end + local Q, R = U:divremainder(T) + if R == Integer.zero() then + factors[#factors+1] = T + U = Q + L = RemoveAll(L, t) + C = RemoveAny(C, t) + else + C = Remove(C, t) + end + end + m = m + 1 + end + if U ~= Integer.one() then + factors[#factors+1] = U + end + return factors +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/rational.lua b/macros/luatex/latex/luacas/tex/algebra/rational.lua new file mode 100644 index 0000000000..811909a948 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/rational.lua @@ -0,0 +1,241 @@ +--- @class Rational +--- Represents an element of the field of rational numbers or rational functions. +--- @field numerator Ring +--- @field denominator Ring +--- @field ring RingIdentifier +Rational = {} +__Rational = {} + + +-------------------------- +-- Static functionality -- +-------------------------- + +-- Metatable for ring objects. +local __obj = {__index = Rational, __eq = function(a, b) + return a["ring"] == b["ring"] and + (a["child"] == b["child"] or a["child"] == nil or b["child"] == nil) and + (a["symbol"] == b["symbol"] or a["child"] == nil or b["child"] == nil) +end, __tostring = function(a) + if a.symbol then + return tostring(a.child.child) .. "(" .. a.symbol .. ")" + end + if a.child then + return "QQ" + end + return "(Generic Fraction Field)" + end} + +--- @param symbol SymbolExpression +--- @param child RingIdentifier +--- @return RingIdentifier +function Rational.makering(symbol, child) + local t = {ring = Rational} + t.symbol = symbol + t.child = child + t = setmetatable(t, __obj) + return t +end + +--- Converts a string of the form -?[0-9]+ or -?[0-9]+\/[0-9]+ to a rational number. +--- @param str string +--- @return Rational|Integer +function Rational.fromstring(str) + local divloc = string.find(str, "/"); + if not divloc then + return Integer(str) + end + return Rational(Integer(string.sub(str, 1, divloc - 1)), Integer(string.sub(str, divloc + 1, #str))) +end + + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- So we don't have to copy the field operations each time. +local __o = Copy(__FieldOperations) +__o.__index = Rational +__o.__tostring = function(a) + if a.ring.symbol then + return "(" .. tostring(a.numerator)..")/("..tostring(a.denominator) .. ")" + end + return tostring(a.numerator).."/"..tostring(a.denominator) +end + +--- Creates a new rational given a numerator and denominator that are part of the same ring. +--- Rational numbers are represented uniquely. +--- @param n Ring +--- @param d Ring +--- @param keep boolean +function Rational:new(n, d, keep) + local o = {} + o = setmetatable(o, __o) + + if n:getring() == PolynomialRing.getring() then + o.symbol = n.symbol + end + + if d:getring() == PolynomialRing.getring() then + o.symbol = d.symbol + end + + if d == Integer(0) then + error("Arithmetic error: division by zero") + end + + n = n or Integer.zero() + d = d or Integer.one() + o.numerator = n + o.denominator = d + o:reduce() + + if (not keep) and o.denominator == Integer.one() or (not keep) and o.numerator == Integer.zero() then + return o.numerator + end + + return o +end + +--- Reduces a rational expression to standard form. This method mutates its object. +function Rational:reduce() + if self.numerator:getring() == Integer.getring() then + if self.denominator < Integer.zero() then + self.denominator = -self.denominator + self.numerator = -self.numerator + end + local gcd = Integer.gcd(self.numerator, self.denominator) + self.numerator = self.numerator//gcd + self.denominator = self.denominator//gcd + self.ring = Integer.getring() + elseif self.numerator:getring() == PolynomialRing.getring() then + local lc = self.denominator:lc() + self.denominator = self.denominator/lc + self.numerator = self.numerator/lc + local gcd = PolynomialRing.gcd(self.numerator, self.denominator) + self.numerator = self.numerator//gcd + self.denominator = self.denominator//gcd + self.ring = Ring.resultantring(self.numerator:getring(), self.denominator:getring()) + end +end + + +--- @return RingIdentifier +function Rational:getring() + local t = {ring=Rational} + if self then + t.child = self.ring + t.symbol = self.symbol + end + t = setmetatable(t, __obj) + return t +end + +--- @param ring RingIdentifier +--- @return Ring +function Rational:inring(ring) + if ring == self:getring() then + return self + end + + if ring == Rational:getring() and ring.symbol then + if not self:getring().symbol then + return Rational(self:inring(ring.child), self:inring(ring.child):one(), true) + end + return Rational(self.numerator:inring(ring.child), self.denominator:inring(ring.child), true) + end + + if ring == PolynomialRing:getring() then + return PolynomialRing({self:inring(ring.child)}, ring.symbol) + end + + error("Unable to convert element to proper ring.") +end + +--- @return boolean +function Rational:isconstant() + if self.symbol then + return false + end + return true +end + +--- @return Expression +function Rational:tocompoundexpression() + return BinaryOperation(BinaryOperation.DIV, {self.numerator:tocompoundexpression(), self.denominator:tocompoundexpression()}) +end + +--- Returns this rational as a floating point number. Can only approximate the value of most rationals. +--- @return number +function Rational:asnumber() + return self.numerator:asnumber() / self.denominator:asnumber() +end + +function Rational:add(b) + return Rational(self.numerator * b.denominator + self.denominator * b.numerator, self.denominator * b.denominator) +end + +function Rational:neg() + return Rational(-self.numerator, self.denominator, true) +end + +function Rational:mul(b) + return Rational(self.numerator * b.numerator, self.denominator * b.denominator) +end + +-- function Rational:inv(b) +-- return Rational(self.numerator * b.numerator, self.denominator * b.denominator) +-- end + +function Rational:pow(b) + return (self.numerator ^ b) / (self.denominator ^ b) +end + +function Rational:div(b) + return Rational(self.numerator * b.denominator, self.denominator * b.numerator) +end + +function Rational:eq(b) + return self.numerator == b.numerator and self.denominator == b.denominator +end + +function Rational:lt(b) + if self.numerator < Integer.zero() and b.numerator > Integer.zero() then + return true + end + if self.numerator > Integer.zero() and b.numerator < Integer.zero() then + return false + end + + if (self.numerator >= Integer.zero() and b.numerator >= Integer.zero()) or (self.numerator <= Integer.zero() and b.numerator <= Integer.zero()) then + return self.numerator * b.denominator < self.denominator * b.numerator + end + return self.numerator * b.denominator > self.denominator * b.numerator +end + +function Rational:le(b) + return self:eq(b) or self:lt(b) +end + +function Rational:zero() + return Integer.zero() +end + +function Rational:one() + return Integer.one() +end + +function Rational:tolatex() + if string.sub(self.numerator:tolatex(),1,1) == '-' then + return "- \\frac{" .. string.sub(self.numerator:tolatex(),2,-1) .. "}{" .. self.denominator:tolatex() .. "}" + end + return "\\frac{" .. self.numerator:tolatex() .."}{".. self.denominator:tolatex().. "}" +end + +----------------- +-- Inheritance -- +----------------- + +__Rational.__index = Field +__Rational.__call = Rational.new +Rational = setmetatable(Rational, __Rational)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/ring.lua b/macros/luatex/latex/luacas/tex/algebra/ring.lua new file mode 100644 index 0000000000..4903b9c912 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/ring.lua @@ -0,0 +1,326 @@ +--- @class Ring +--- Interface for an element of a ring with unity. +Ring = {} +__Ring = {} + +-------------------------- +-- Static functionality -- +-------------------------- + +--- Determines which ring the output of a binary operation with inputs in ring1 and ring2 should be, if such a ring exists. +--- If one of the rings is a subring of another ring, the result should be one of the two rings. +--- @param ring1 RingIdentifier +--- @param ring2 RingIdentifier +--- @return RingIdentifier +function Ring.resultantring(ring1, ring2) + if ring1 == ring2 then + return ring1 + end + + if ((ring1 == PolynomialRing.getring() and ring2 == Rational.getring()) or + (ring2 == PolynomialRing.getring() and ring1 == Rational.getring())) + and ring1.symbol == ring2.symbol then + return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child)) + end + + if ring1 == PolynomialRing.getring() or ring2 == PolynomialRing.getring() then + if ring1 == ring2.child then + return ring2 + end + if ring2 == ring1.child then + return ring1 + end + + if ring1 == PolynomialRing.getring() and ring2 == PolynomialRing.getring() and ring1.symbol == ring2.symbol then + return PolynomialRing.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child)) + end + + -- If none of the above conditions are satisfied, recusion is a pain, so we just strip all of the variables off of both rings. + -- TODO: Make this properly recursive, or just use a multivariable polynomial ring class + local symbols = {} + while ring1 == PolynomialRing.getring() do + symbols[#symbols+1] = ring1.symbol + ring1 = ring1.child + end + while ring2 == PolynomialRing.getring() do + if not Contains(symbols, ring2.symbol) then + symbols[#symbols+1] = ring2.symbol + end + ring2 = ring2.child + end + local ring = Ring.resultantring(ring1, ring2) + + if ring == Rational.getring() and Contains(symbols, ring.symbol) then + symbols = Remove(symbols, ring.symbol) + end + for i = #symbols, 1, -1 do + ring = PolynomialRing.makering(symbols[i], ring) + end + return ring + end + + if ring1 == Integer.getring() then + if ring2 == Integer.getring() then + return ring2 + end + + if ring2 == Rational.getring() then + return ring2 + end + + if ring2 == IntegerModN.getring() then + return ring2 + end + end + + if ring1 == Rational.getring() then + if ring2 == Integer.getring() then + return ring1 + end + + if ring2 == Rational.getring() then + if not ring1.symbol then + return Rational.makering(ring2.symbol, Ring.resultantring(ring1, ring2.child)) + end + if not ring2.symbol then + return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2)) + end + if ring1.symbol and ring2.symbol and ring1.symbol == ring2.symbol then + return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child)) + end + return ring2 + end + + if ring2 == IntegerModN.getring() then + return nil + end + end + + if ring1 == IntegerModN.getring() then + if ring2 == Integer.getring() then + return ring1 + end + + if ring2 == Rational.getring() then + return nil + end + + if ring2 == IntegerModN.getring() then + return IntegerModN.makering(Integer.gcd(ring1.modulus, ring2.modulus)) + end + end + + return nil +end + +--- Returns a particular instantiation of a ring. +--- Does the same thing as getring() if there is only one possible ring for a class, i.e., the integers and rationals. +--- @return RingIdentifier +function Ring.makering() + error("Called unimplemented method : makering()") +end + +---------------------- +-- Required methods -- +---------------------- + +--- Returns the ring this element is part of. +--- @return RingIdentifier +function Ring:getring() + error("Called unimplemented method : getring()") +end + +--- Explicitly converts this element to an element of another ring. +--- @param ring RingIdentifier +--- @return Ring +function Ring:inring(ring) + error("Called unimplemented method : in()") +end + +--- Returns whether the ring is commutative. +--- @return boolean +function Ring:iscommutative() + error("Called unimplemented method : iscommutative()") +end + +--- @return Ring +function Ring:add(b) + error("Called unimplemented method : add()") +end + +--- @return Ring +function Ring:sub(b) + return(self:add(b:neg())) +end + +--- @return Ring +function Ring:neg() + error("Called unimplemented method : neg()") +end + +--- @return Ring +function Ring:mul(b) + error("Called unimplemented method : mul()") +end + +--- Ring exponentiation by definition. Specific rings may implement more efficient methods. +--- @return Ring +function Ring:pow(n) + if(n < Integer.zero()) then + error("Execution error: Negative exponentiation is undefined over general rings") + end + local k = Integer.zero() + local b = self:one() + while k < n do + b = b * self + k = k + Integer.one() + end + return b +end + +--- @return boolean +function Ring:eq(b) + error("Execution error: Ring does not have a total order") +end + +--- @return boolean +function Ring:lt(b) + error("Execution error: Ring does not have a total order") +end + +--- @return boolean +function Ring:le(b) + error("Execution error: Ring does not have a total order") +end + +--- The additive identitity of the ring. +--- @return Ring +function Ring:zero() + error("Called unimplemented method : zero()") +end + +--- The multiplicative identitity of the ring. +--- @return Ring +function Ring:one() + error("Called unimplemented method : one()") +end + +-------------------------- +-- Instance metamethods -- +-------------------------- +__RingOperations = {} + +-- Each of these methods just handles coverting each element in the ring to an instance of the proper ring, if possible, +-- then passing the arguments to the function in a specific ring. + +__RingOperations.__unm = function(a) + return a:neg() +end + +__RingOperations.__add = function(a, b) + if not b.getring then + return BinaryOperation.ADDEXP({a, b}) + end + + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to add two elements of incompatable rings") + end + return a:inring(oring):add(b:inring(oring)) +end + +__RingOperations.__sub = function(a, b) + if not b.getring then + return BinaryOperation.SUBEXP({a, b}) + end + + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to subtract two elements of incompatable rings") + end + return a:inring(oring):sub(b:inring(oring)) +end + +-- Allows for multiplication by writing two expressions next to each other. +__RingOperations.__call = function (a, b) + return a * b +end + +__RingOperations.__mul = function(a, b) + if not b.getring then + return BinaryOperation.MULEXP({a, b}) + end + + local aring, bring = a:getring(), b:getring() + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to muliply two elements of incompatable rings") + end + return a:inring(oring):mul(b:inring(oring)) +end + +__RingOperations.__pow = function(a, n) + if (not n.getring) or (n.getring and n:getring().ring ~= Integer) then + return BinaryOperation.POWEXP({a, n}) + end + + -- if a == a:zero() and n == Integer.zero() then + -- error("Cannot raise 0 to the power of 0") + -- end + + return a:pow(n) +end + +-- Comparison operations assume, of course, that the ring operation is equipped with a total order +-- All elements of all rings need these metamethods, since in Lua comparisons on tables only fire if both objects have the table +__RingOperations.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not a.getring or not b.getring then + return false + end + local aring, bring = a:getring(), b:getring() + if aring == bring then + return a:eq(b) + end + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to compare two elements of incompatable rings") + end + return a:inring(oring):eq(b:inring(oring)) +end + +__RingOperations.__lt = function(a, b) + local aring, bring = a:getring(), b:getring() + if aring == bring then + return a:lt(b) + end + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to compare two elements of incompatable rings") + end + return a:inring(oring):lt(b:inring(oring)) +end + +__RingOperations.__le = function(a, b) + local aring, bring = a:getring(), b:getring() + if aring == bring then + return a:le(b) + end + local oring = Ring.resultantring(aring, bring) + if not oring then + error("Attempted to compare two elements of incompatable rings") + end + return a:inring(oring):le(b:inring(oring)) +end + +----------------- +-- Inheritance -- +----------------- + +__Ring.__index = ConstantExpression +Ring = setmetatable(Ring, __Ring) + +--- Used for comparing and converting between rings. +--- @class RingIdentifier diff --git a/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua b/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua new file mode 100644 index 0000000000..a66182579a --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua @@ -0,0 +1,135 @@ +--- @class RootExpression +--- An expression that represents the solutions to expression = 0. +--- @field expression Expression +RootExpression = {} +__RootExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new root expression with the given expression. +--- @param expression Expression +--- @return RootExpression +function RootExpression:new(expression) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.expression = Copy(expression) + + __o.__index = RootExpression + __o.__tostring = function(a) + return 'Root Of: (' .. tostring(a.expression) .. ')' + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway + if not b:type() == RootExpression then + return false + end + return a.expression == b.expression + end + o = setmetatable(o, __o) + + return o +end + +--- @return Expression +function RootExpression:autosimplify(subpart) + local simplified = self.expression:autosimplify() + local simplified, ispoly = simplified:topolynomial() + + if simplified:isconstant() then + -- 0 = 0 is always true (obviously). + return simplified == simplified:zero() + end + + if ispoly then + if simplified.degree == Integer.zero() then + return simplified == simplified:zero() + end + if simplified.degree == Integer.one() then + return {-simplified.coefficients[0] / simplified.coefficients[1]} + end + if simplified.degree == Integer(2) then + local a = simplified.coefficients[2] + local b = simplified.coefficients[1] + local c = simplified.coefficients[0] + -- This is a hack until we can get more expression manipulation working, but that's okay. + if subpart then + c = (c - subpart):autosimplify() + end + return {((-b + sqrt(b^Integer(2) - Integer(4) * a * c)) / (Integer(2) * a)):autosimplify(), + ((-b - sqrt(b^Integer(2) - Integer(4) * a * c)) / (Integer(2) * a)):autosimplify()} + end + if simplified.degree == Integer(3) then + local a = simplified.coefficients[3] + local b = simplified.coefficients[2] + local c = simplified.coefficients[1] + local d = simplified.coefficients[0] + -- This is a hack until we can get more expression manipulation working, but that's okay. + if subpart then + d = (d - subpart):autosimplify() + end + + local delta0 = (b^Integer(2) - Integer(3)*a*c):autosimplify() + local delta1 = (Integer(2) * b^Integer(3) - Integer(9)*a*b*c+Integer(27)*a^Integer(2)*d):autosimplify() + + local C = sqrt((delta1 + sqrt(delta1 ^ Integer(2) - Integer(4) * delta0 ^ Integer(3))) / Integer(2), Integer(3)):autosimplify() + + if C == Integer.zero() then + C = sqrt((delta1 - sqrt(delta1 ^ Integer(2) - Integer(4) * delta0 ^ Integer(3))) / Integer(2), Integer(3)):autosimplify() + end + + if C == Integer.zero() then + C = (-b/(Integer(3)*a)):autosimplify() + end + + local eta = ((Integer(-1) + sqrt(Integer(-3))) / Integer(2)):autosimplify() + + return {((-Integer.one() / (Integer(3) * a)) * (b + C + delta0 / C)):autosimplify(), + ((-Integer.one() / (Integer(3) * a)) * (b + C*eta + delta0 / (C*eta))):autosimplify(), + ((-Integer.one() / (Integer(3) * a)) * (b + C*eta^Integer(2) + delta0 / (C*eta^Integer(2)))):autosimplify()} + end + end + if ispoly then + simplified = simplified:autosimplify() + end + if subpart then + simplified = (simplified - subpart):autosimplify() + end + return {RootExpression(simplified)} +end + +--- @return table<number, Expression> +function RootExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return RootExpression +function RootExpression:setsubexpressions(subexpressions) + return RootExpression(subexpressions[1]) +end + +--- @param other Expression +--- @return boolean +function RootExpression:order(other) + --- TODO: Fix ordering on new expression types + if other:type() ~= RootExpression then + return false + end + + return self.expression:order(other.expression) +end + +--- @return string +function RootExpression:tolatex() + return '\\operatorname{RootOf}\\left(' .. self.expression:tolatex() .. '\\right)' +end + +----------------- +-- Inheritance -- +----------------- +__RootExpression.__index = CompoundExpression +__RootExpression.__call = RootExpression.new +RootExpression = setmetatable(RootExpression, __RootExpression)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua b/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua new file mode 100644 index 0000000000..bc96f4e1d8 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua @@ -0,0 +1,196 @@ +--- @class SqrtExpression +--- An expression that represents the positive real solution to x^n = a where n is a positive integer and a is constant. +--- @field expression Expression +SqrtExpression = {} +__SqrtExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new sqrt expression with the given expression. +--- @param expression Expression +--- @param root Integer +--- @return SqrtExpression +function SqrtExpression:new(expression, root) + root = root or Integer(2) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.expression = Copy(expression) + o.root = root + + __o.__index = SqrtExpression + __o.__tostring = function(a) + return tostring(a.expression) .. ' ^ (1/' .. tostring(a.root) .. ')' + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway + if not b:type() == SqrtExpression then + return false + end + return a.expression == b.expression and a.root == b.root + end + o = setmetatable(o, __o) + + return o +end + + +--- @return table<number, Expression> +function SqrtExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return SqrtExpression +function SqrtExpression:setsubexpressions(subexpressions) + return SqrtExpression(subexpressions[1], self.root) +end + +--- @param other Expression +--- @return boolean +function SqrtExpression:order(other) + return self:topower():order(other) +end + +function SqrtExpression:topower() + local exponent = BinaryOperation(BinaryOperation.DIV,{Integer.one(),self.root}):autosimplify() + local base = self.expression + return BinaryOperation(BinaryOperation.POW,{base,exponent}):autosimplify() +end + +function SqrtExpression:autosimplify() + local expression = self.expression:autosimplify() + local root = self.root:autosimplify() + + if root == Integer.one() then + return expression + end + + if root:type() == Rational then + return SqrtExpression(BinaryOperation(BinaryOperation.POW,{expression,root.denominator}):autosimplify(), root.numerator):autosimplify() + end + + if not root:isconstant() then + return BinaryOperation(BinaryOperation.POW,{expression,Integer.one() / root}):autosimplify() + end + + if not expression:isconstant() then + if expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() then + local coeff = SqrtExpression(expression.expressions[1],root):autosimplify() + expression.expressions[1] = BinaryOperation(BinaryOperation.MUL,{Integer.one()}) + expression = expression:autosimplify() + local sqrtpart = SqrtExpression(expression,root):autosimplify() + local result = coeff*sqrtpart + return result:autosimplify() + end + return BinaryOperation(BinaryOperation.POW,{expression,Integer.one() / root}):autosimplify() + end + + if expression:type() == Rational then + local result = BinaryOperation(BinaryOperation.MUL, {SqrtExpression(expression.numerator,root):autosimplify(),BinaryOperation(BinaryOperation.POW,{SqrtExpression(expression.denominator,root):autosimplify(),Integer(-1)})}) + return result:autosimplify() + end + + if expression:type() == Integer then + if expression == Integer.zero() then + return Integer.zero() + end + if expression == Integer.one() then + return Integer.one() + end + if expression < Integer.zero() then + if root == Integer(2) then + local result = SqrtExpression(expression:neg(),root):autosimplify() + result = I*result + return result:autosimplify() + end + if root % Integer(2) == Integer.one() then + local result = SqrtExpression(expression:neg(),root):autosimplify() + result = -result + return result:autosimplify() + end + end + local primes = expression:primefactorization() + local coeffresult = {} + local exprresult = {} + local reduction = root + for _, term in ipairs(primes.expressions) do + local primepower = term.expressions[2] + reduction = Integer.gcd(primepower,reduction) + if reduction == Integer.one() then + goto skip + end + end + ::skip:: + local newroot = root / reduction + for index, term in ipairs(primes.expressions) do + local prime = term.expressions[1] + local primepower = term.expressions[2] / reduction + local coeffpower = primepower // newroot + coeffresult[index] = prime ^ coeffpower + local exprpower = primepower - coeffpower*newroot + exprresult[index] = prime ^ exprpower + end + local newexpression = BinaryOperation(BinaryOperation.MUL,exprresult):autosimplify() + local coeff = BinaryOperation(BinaryOperation.MUL,coeffresult):autosimplify() + if coeff == Integer.one() then + if reduction == Integer.one() then + goto stop + end + return SqrtExpression(newexpression,newroot) + end + if newroot == Integer.one() then + return coeff + end + return BinaryOperation(BinaryOperation.MUL,{coeff,SqrtExpression(newexpression,newroot)}):autosimplify() + end + ::stop:: + + if expression.operation == BinaryOperation.POW and expression.expressions[2]:type() == Integer then + local exponent = expression.expressions[2] + local power = exponent // root + local newexponent = (exponent / root) - power + local coeff = expression.expressions[1] ^ power + coeff = coeff:evaluate() + if newexponent == Integer.zero() then + return coeff + else + local num = newexponent.numerator + local den = newexponent.denominator + local newexpression = expression ^ num + newexpression = newexpression:autosimplify() + local result = coeff * SqrtExpression(newexpression,den) + return result + end + end + + return SqrtExpression(expression,root) +end + +function SqrtExpression:tolatex() + local printout = '\\sqrt' + if self.root == Integer(2) then + printout = printout .. '{' .. self.expression:tolatex() .. '}' + else + printout = printout .. '[' .. self.root:tolatex() .. ']' .. '{' .. self.expression:tolatex() .. '}' + end + return printout +end + + +----------------- +-- Inheritance -- +----------------- +__SqrtExpression.__index = CompoundExpression +__SqrtExpression.__call = SqrtExpression.new +SqrtExpression = setmetatable(SqrtExpression, __SqrtExpression) + +---------------------- +-- Static constants -- +---------------------- + +sqrt = function(expression, root) + return SqrtExpression(expression, root) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua b/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua new file mode 100644 index 0000000000..307bfe1737 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua @@ -0,0 +1,355 @@ +--- @class TrigExpression +--- Represents a trigonometric function from one expression to another. +--- @field name string +--- @field expression Expression +TrigExpression = {} +__TrigExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new trig expression with the given name and expression. +--- @param name string|SymbolExpression +--- @param expression Expression +--- @return TrigExpression +function TrigExpression:new(name, expression) + local o = {} + local __o = Copy(__ExpressionOperations) + + if not TrigExpression.NAMES[name] then + error("Argument error: " .. name .. " is not the name of a trigonometric function.") + end + + o.name = name + o.expression = expression + o.expressions = {expression} + if expression:isatomic() then + o.variables = {expression} + else + o.variables = {SymbolExpression('x')} + end + o.derivatives = {Integer.zero()} + + __o.__index = TrigExpression + __o.__tostring = function(a) + return tostring(a.name) .. '(' .. tostring(a.expression) .. ')' + end + __o.__eq = function(a, b) + -- if b:type() == FunctionExpression then + -- return a:tofunction() == b + -- end + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not b:type() == TrigExpression then + return false + end + return a.name == b.name and a.expression == b.expression + end + + o = setmetatable(o, __o) + return o +end + +--- @return TrigExpression +function TrigExpression:evaluate() + local expression = self.expression:autosimplify() + + if expression == Integer.zero() then + if self.name == "cos" or self.name == "sec" then + return Integer.one() + end + if self.name == "sin" or self.name == "tan" then + return Integer.zero() + end + if self.name == "arctan" or self.name == "arcsin" then + return Integer.zero() + end + if self.name == "arccos" or self.name == "arccot" then + return PI / Integer(2) + end + end + + if expression == PI then + if self.name == "cos" or self.name == "sec" then + return Integer(-1) + end + if self.name == "sin" or self.name == "tan" then + return Integer.zero() + end + end + + if expression:ismulratlPI() then + local coeff = expression.expressions[1] + if TrigExpression.COSVALUES[tostring(coeff)] ~= nil then + if self.name == "cos" then + return TrigExpression.COSVALUES[tostring(coeff)]:autosimplify() + end + if self.name == "sin" then + local sign = Integer.one() + if coeff > Integer.one() then + sign = Integer(-1) + end + return (sign*sqrt(Integer.one()-cos(expression)^Integer(2))):autosimplify() + end + if self.name == "tan" then + return (sin(expression) / cos(expression)):autosimplify() + end + if self.name == "sec" then + return (Integer.one() / cos(expression)):autosimplify() + end + if self.name == "csc" then + return (Integer.one() / sin(expression)):autosimplify() + end + if self.name == "cot" then + return (cos(expression) / sin(expression)):autosimplify() + end + end + end + + if TrigExpression.ACOSVALUES[tostring(expression)] ~= nil then + if self.name == "arccos" then + return TrigExpression.ACOSVALUES[tostring(expression)]:autosimplify() + end + if self.name == "arcsin" then + if expression == Integer(-1) then + return TrigExpression.ACOSVALUES["-1"]:autosimplify() + elseif expression.expressions and expression.expressions[1] == Integer(-1) then + local expr = (Integer(-1)*sqrt(Integer.one() - expression ^ Integer(2))):autosimplify() + return TrigExpression.ACOSVALUES[tostring(expr)]:autosimplify() + else + local expr = (sqrt(Integer.one() - expression ^ Integer(2))):autosimplify() + return TrigExpression.ACOSVALUES[tostring(expr)]:autosimplify() + end + end + end + + if self.name == "arctan" and TrigExpression.ATANVALUES[tostring(expression)] ~= nil then + return TrigExpression.ATANVALUES[tostring(expression)]:autosimplify() + end + + return self +end + +--- checks if expression is a rational multiple of pi +--- @return boolean +function Expression:ismulratlPI() + if self.operation == BinaryOperation.MUL and #self.expressions == 2 and (self.expressions[1]:type() == Integer or self.expressions[1]:type() == Rational) and self.expressions[2] == PI then + return true + end + + return false +end + +--- @return TrigExpression +function TrigExpression:autosimplify() + local expression = self.expression:autosimplify() + + -- even and odd properties of trig functions + if (self.name == "sin" or self.name == "tan" or self.name == "csc" or self.name == "cot") and + expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() and expression.expressions[1] < Integer(0) then + return (-Integer.one() * TrigExpression(self.name, -expression)):autosimplify() + end + + if (self.name == "cos" or self.name == "sec") and + expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() and expression.expressions[1] < Integer(0) then + expression = (-expression):autosimplify() + end + + -- uses periodicity of sin and cos and friends + if self.name == "sin" or self.name == "cos" or self.name == "csc" or self.name == "sec" then + if expression == Integer.zero() or expression == PI then + goto skip + end + if expression.operation ~= BinaryOperation.ADD then + expression = BinaryOperation(BinaryOperation.ADD,{expression}) + end + for index,component in ipairs(expression.expressions) do + if component:ismulratlPI() then + local coeff = component.expressions[1] + if coeff:type() == Integer then + coeff = coeff % Integer(2) + coeff = coeff:autosimplify() + end + if coeff:type() == Rational then + local n = coeff.numerator + local d = coeff.denominator + local m = {n:divremainder(d)} + coeff = (m[1] % Integer(2)) + m[2]/d + coeff = coeff:autosimplify() + end + expression.expressions[index].expressions[1] = coeff + end + expression = expression:autosimplify() + end + ::skip:: + end + + -- uses periodicity of tan and cot + if self.name == "tan" or self.name == "cot" then + if expression == Integer.zero() or expression == PI then + goto skip + end + if expression.operation ~= BinaryOperation.ADD then + expression = BinaryOperation(BinaryOperation.ADD,{expression}) + end + for index,component in ipairs(expression.expressions) do + if component:ismulratlPI() then + local coeff = component.expressions[1] + if coeff:type() == Integer then + coeff = Integer.zero() + end + if coeff:type() == Rational then + local n = coeff.numerator + local d = coeff.denominator + local m = {n:divremainder(d)} + coeff = m[2]/d + coeff = coeff:autosimplify() + end + expression.expressions[index].expressions[1] = coeff + end + if component == PI then + expression.expressions[index] = Integer.zero() + end + end + expression = expression:autosimplify() + ::skip:: + end + + return TrigExpression(self.name, expression):evaluate() +end + +--- @return table<number, Expression> +function TrigExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return TrigExpression +function TrigExpression:setsubexpressions(subexpressions) + return TrigExpression(self.name, subexpressions[1]) +end + +-- function TrigExpression:freeof(symbol) +-- return self.expression:freeof(symbol) +-- end + +-- function TrigExpression:substitute(map) +-- for expression, replacement in pairs(map) do +-- if self == expression then +-- return replacement +-- end +-- end +-- return TrigExpression(self.name, self.expression:substitute(map)) +-- end + +-- function TrigExpression:order(other) +-- return self:tofunction():order(other) +-- end + +-- function TrigExpression:tofunction() +-- return FunctionExpression(self.name, {self.expression}, true) +-- end + +----------------- +-- Inheritance -- +----------------- + +__TrigExpression.__index = FunctionExpression +__TrigExpression.__call = TrigExpression.new +TrigExpression = setmetatable(TrigExpression, __TrigExpression) + +---------------------- +-- Static constants -- +---------------------- +TrigExpression.NAMES = {sin=1, cos=2, tan=3, csc=4, sec=5, cot=6, + arcsin=7, arccos=8, arctan=9, arccsc=10, arcsec=11, arccot=12} + +TrigExpression.INVERSES = {sin="arcsin", cos="arccos", tan="arctan", csc="arccsc", sec="arcsec", cot="arccot", + arcsin="sin", arccos="cos", arctan="tan", arccsc="csc", arcsec="sec", arccot="cot"} + +TrigExpression.COSVALUES = { + ["0"] = Integer.one(), + ["1/6"] = sqrt(Integer(3))/Integer(2), + ["1/4"] = sqrt(Integer(2))/Integer(2), + ["1/3"] = Integer.one()/Integer(2), + ["1/2"] = Integer.zero(), + ["2/3"] = -Integer.one()/Integer(2), + ["3/4"] = -sqrt(Integer(2))/Integer(2), + ["5/6"] = -sqrt(Integer(3))/Integer(2), + ["1"] = -Integer.one(), + ["7/6"] = -sqrt(Integer(3))/Integer(2), + ["5/4"] = -sqrt(Integer(2))/Integer(2), + ["4/3"] = -Integer.one()/Integer(2), + ["3/2"] = Integer.zero(), + ["5/3"] = Integer.one()/Integer(2), + ["7/4"] = sqrt(Integer(2))/Integer(2), + ["11/6"] = sqrt(Integer(3))/Integer(2), +} +TrigExpression.ACOSVALUES = { + ["1"] = Integer.zero(), + ["(1/2 * sqrt(3,2))"] = PI * Integer(6) ^ Integer(-1), + ["(1/2 * sqrt(2,2))"] = PI * Integer(4) ^ Integer(-1), + ["1/2"] = PI * Integer(3) ^ Integer(-1), + ["0"] = PI * Integer(2) ^ Integer(-1), + ["-1/2"] = PI * Integer(2) * Integer(3) ^ Integer(-1), + ["(-1/2 * sqrt(2,2))"]= PI * Integer(3) * Integer(4) ^ Integer(-1), + ["(-1/2 * sqrt(3,2))"]= PI * Integer(5) * Integer(6) ^ Integer(-1), + ["-1"] = Integer(-1)*PI, +} +TrigExpression.ATANVALUES = { + ["(-1 * sqrt(3,2))"] = Integer(-1) * PI * Integer(3) ^ Integer(-1), + ["-1"] = Integer(-1) * PI * Integer(4) ^ Integer(-1), + ["(-1/3 * sqrt(3,2))"] = Integer(-1) * Integer(6) ^ Integer(-1), + ["0"] = Integer.zero(), + ["(1/3 * sqrt(3,2))"] = PI * Integer(6) ^ Integer(-1), + ["1"] = PI * Integer(4) ^ Integer(-1), + ["sqrt(3,2)"] = PI * Integer(3) ^ Integer(-1) +} + +SIN = function (a) + return TrigExpression("sin", a) +end + +COS = function (a) + return TrigExpression("cos", a) +end + +TAN = function (a) + return TrigExpression("tan", a) +end + +CSC = function (a) + return TrigExpression("csc", a) +end + +SEC = function (a) + return TrigExpression("sec", a) +end + +COT = function (a) + return TrigExpression("cot", a) +end + +ARCSIN = function (a) + return TrigExpression("arcsin", a) +end + +ARCCOS = function (a) + return TrigExpression("arccos", a) +end + +ARCTAN = function (a) + return TrigExpression("arctan", a) +end + +ARCCSC = function (a) + return TrigExpression("arccsc", a) +end + +ARCSEC = function (a) + return TrigExpression("arcsec", a) +end + +ARCCOT = function (a) + return TrigExpression("arccot", a) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/calculus/_init.lua b/macros/luatex/latex/luacas/tex/calculus/_init.lua new file mode 100644 index 0000000000..59ba1bbcc8 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/calculus/_init.lua @@ -0,0 +1,6 @@ +-- Loads calculus files in the correct order. +require("algebra._init") + +require("calculus.derivativeexpression") +require("calculus.integralexpression") +require("calculus.diffexpression") diff --git a/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua b/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua new file mode 100644 index 0000000000..b757759c4c --- /dev/null +++ b/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua @@ -0,0 +1,265 @@ +--- @class DerivativeExpression +--- An expression for a single-variable derivative of an expression. +--- @field symbol SymbolExpression +--- @field expression Expression +DerivativeExpression = {} +__DerivativeExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- Creates a new single-variable derivative operation with the given symbol and expression. +--- @param expression Expression +--- @param symbol Symbol +--- @return DerivativeExpression +function DerivativeExpression:new(expression, symbol) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.expression = Copy(expression) + o.symbol = symbol or SymbolExpression("x") + + __o.__index = DerivativeExpression + __o.__tostring = function(a) + return '(d/d' .. tostring(a.symbol) .. " " .. tostring(a.expression) .. ')' + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not b:type() == DerivativeExpression then + return false + end + return a.symbol == b.symbol and a.expression == b.expression + end + o = setmetatable(o, __o) + + return o +end + +--- @return Expression +function DerivativeExpression:evaluate() + local exp = self.expression + + -- The derivative of a constant is 0 + if exp:isconstant() then + return Integer.zero() + end + + -- The derivative of a symbol is either 1 or 0 + if exp:type() == SymbolExpression then + if self.symbol == exp then + return Integer.one() + end + return Integer.zero() + end + + -- Chain rule for arbitrary functions + + if exp:type() == FunctionExpression then + local results = {} + for index,expression in ipairs(exp.expressions) do + local dout = FunctionExpression(exp.name,exp.expressions,exp.derivatives) + dout.variables = exp.variables + dout.derivatives[index] = dout.derivatives[index]+Integer.one() + local dinn = DerivativeExpression(expression,self.symbol):evaluate() + results[index] = dout*dinn + end + return BinaryOperation(BinaryOperation.ADD,results):autosimplify() + end + + --if exp:type() == FunctionExpression then + -- local results = {} + -- for index,expression in ipairs(exp.expressions) do + -- local inn = DerivativeExpression(expression,self.symbol):autosimplify() + -- local out = Copy(exp) + -- out.orders[index] = out.orders[index] + Integer.one() + -- local result = inn*out + -- table.insert(results,result) + -- end + -- return BinaryOperation(BinaryOperation.ADD,results):autosimplify() + --end + + --if exp:type() == FunctionExpression then + -- if exp.expressions[2] then + -- return self + -- end + -- return DerivativeExpression(exp.expressions[1], self.symbol) * FunctionExpression(exp.name, exp.expressions, exp.orders[1] + Integer.one(), exp.variables[1]):autosimplify() + --end + + -- Chain rule for trig functions + if exp:type() == TrigExpression then + local internal = DerivativeExpression(exp.expression, self.symbol) + + if exp.name == "sin" then + return (internal * COS(exp.expression)):autosimplify() + end + if exp.name == "cos" then + return (internal * -SIN(exp.expression)):autosimplify() + end + if exp.name == "tan" then + return (internal * SEC(exp.expression)^Integer(2)):autosimplify() + end + if exp.name == "csc" then + return (internal * -CSC(exp.expression)*COT(exp.expression)):autosimplify() + end + if exp.name == "sec" then + return (internal * -SEC(exp.expression)*TAN(exp.expression)):autosimplify() + end + if exp.name == "cot" then + return (internal * -CSC(exp.expression)^Integer(2)):autosimplify() + end + if exp.name == "arcsin" then + return (internal / (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2))):autosimplify() + end + if exp.name == "arccos" then + return (-internal / (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2))):autosimplify() + end + if exp.name == "arctan" then + return (internal / (Integer(1)+exp.expression^Integer(2))):autosimplify() + end + if exp.name == "arccsc" then + return (-internal / (ABS(exp.expression) * (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2)))):autosimplify() + end + if exp.name == "arcsec" then + return (internal / (ABS(exp.expression) * (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2)))):autosimplify() + end + if exp.name == "arccot" then + return (-internal / (Integer(1)+exp.expression^Integer(2))):autosimplify() + end + end + + -- TODO: Piecewise functions + if self:type() == AbsExpression then + return DerivativeExpression(self.expression, self.symbol):autosimplify() + end + + -- Uses linearity of derivatives to evaluate sum expressions + if exp.operation == BinaryOperation.ADD then + local parts = {} + for i, expression in pairs(exp.expressions) do + parts[i] = DerivativeExpression(expression, self.symbol) + end + return BinaryOperation(BinaryOperation.ADD, parts):autosimplify() + end + + -- Uses product rule to evaluate product expressions + if exp.operation == BinaryOperation.MUL then + local sums = {} + for i, expression in pairs(exp.expressions) do + local products = {} + for j, innerexpression in pairs(exp.expressions) do + if i ~= j then + products[j] = innerexpression + else + products[j] = DerivativeExpression(innerexpression, self.symbol) + end + end + sums[i] = BinaryOperation(BinaryOperation.MUL, products) + end + return BinaryOperation(BinaryOperation.ADD, sums):autosimplify() + end + + -- Uses the generalized power rule to evaluate power expressions + if exp.operation == BinaryOperation.POW then + local base = exp.expressions[1] + local exponent = exp.expressions[2] + + return BinaryOperation.MULEXP({ + BinaryOperation.POWEXP({base, exponent}), + BinaryOperation.ADDEXP({ + BinaryOperation.MULEXP({ + DD(base, self.symbol), + BinaryOperation.DIVEXP({exponent, base})}), + BinaryOperation.MULEXP({ + DD(exponent, self.symbol), + LN(base)})}) + }):autosimplify() + end + + if exp:type() == Logarithm then + local base = exp.base + local expression = exp.expression + + return BinaryOperation.SUBEXP({ + BinaryOperation.DIVEXP({DD(expression, self.symbol), + BinaryOperation.MULEXP({expression, LN(base)})}), + + BinaryOperation.DIVEXP({ + BinaryOperation.MULEXP({LN(expression), DD(base, self.symbol)}), + BinaryOperation.MULEXP({BinaryOperation.POWEXP({LN(base), Integer(2)}), base}) + }) + + }):autosimplify() + end + + return self +end + +--- @return Expression +function DerivativeExpression:autosimplify() + return DerivativeExpression(self.expression:autosimplify(), self.symbol):evaluate() +end + +--- @return table<number, Expression> +function DerivativeExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return DerivativeExpression +function DerivativeExpression:setsubexpressions(subexpressions) + return DerivativeExpression(subexpressions[1], self.symbol) +end + +-- function DerivativeExpression:freeof(symbol) +-- return self.symbol.freeof(symbol) and self.expression:freeof(symbol) +-- end + +-- Substitutes each expression for a new one. +-- function DerivativeExpression:substitute(map) +-- for expression, replacement in pairs(map) do +-- if self == expression then +-- return replacement +-- end +-- end +-- -- Typically, we only perform substitution on autosimplified expressions, so this won't get called. May give strange results, i.e., +-- -- substituting and then evaluating the derivative may not return the same thing as evaluating the derivative and then substituting. +-- return DerivativeExpression(self.expression:substitute(map), self.symbol) +-- end + +--- @param other Expression +--- @return boolean +function DerivativeExpression:order(other) + if other:type() == IntegralExpression then + return true + end + + if other:type() ~= DerivativeExpression then + return false + end + + if self.symbol ~= other.symbol then + return self.symbol:order(other.symbol) + end + + return self.expression:order(other.expression) +end + +--- @return string +function DerivativeExpression:tolatex() + return '\\frac{d}{d' .. self.symbol:tolatex() .. '}\\left(' .. self.expression:tolatex() .. '\\right)' +end + +----------------- +-- Inheritance -- +----------------- +__DerivativeExpression.__index = CompoundExpression +__DerivativeExpression.__call = DerivativeExpression.new +DerivativeExpression = setmetatable(DerivativeExpression, __DerivativeExpression) + +---------------------- +-- Static constants -- +---------------------- +DD = function(expression, symbol) + return DerivativeExpression(expression, symbol) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua b/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua new file mode 100644 index 0000000000..df1dc90f33 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua @@ -0,0 +1,190 @@ +--- @class DiffExpression +--- An expression for a multi-variable higher-order derivatives of an expression. +--- @field symbols SymbolExpression +--- @field expression Expression + +DiffExpression = {} +__DiffExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +-- Creates a new derivative operation with the given symbols and expression. +--- @param expression Expression +--- @param symbols table<number, Symbol> +--- @return DiffExpression +function DiffExpression:new(expression,symbols) + local o = {} + local __o = Copy(__ExpressionOperations) + + o.symbols = Copy(symbols) + o.degree = #o.symbols + o.expression = Copy(expression) + + __o.__tostring = function(a) + local varlist = '(d' + if a.degree == 1 then + varlist = varlist .. '/d' .. tostring(a.symbols[1]) .. " " .. tostring(a.expression) .. ')' + end + if a.degree > 1 then + varlist = varlist .. '^' .. tostring(a.degree) .. '/' + local varnum = 1 + for index = 1, #a.symbols do + local var = a.symbols[#a.symbols-index+1] + if a.symbols[#a.symbols-index] == var then + varnum = varnum + 1 + goto nextvar + end + if a.symbols[#a.symbols-index] ~=var then + if varnum == 1 then + varlist = varlist .. 'd' .. tostring(var) + else + varlist = varlist .. 'd' .. tostring(var) .. '^' .. tostring(varnum) + end + varnum = 1 + end + ::nextvar:: + end + varlist = varlist .. " " .. tostring(a.expression) .. ')' + end + return varlist + end + + __o.__index = DiffExpression + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway + if not b:type() == DiffExpression then + return false + end + if a.expression ~= b.expression then + return false + end + local loc = 1 + while a.symbols[loc] or b.symbols[loc] do + if not a.symbols[loc] or not b.symbols[loc] or + (a.symbols[loc] ~= b.symbols[loc]) then + return false + end + loc = loc + 1 + end + return true + end + o = setmetatable(o, __o) + + return o +end + +--- @return Expression +function DiffExpression:evaluate() + local exp = self.expression + + for _,var in ipairs(self.symbols) do + exp = DerivativeExpression(exp,var):evaluate() + end + return exp +end + +--- @return Expression +function DiffExpression:autosimplify() + return DiffExpression(self.expression:autosimplify(), self.symbols):evaluate() +end + + +--- @return table<number, Expression> +function DiffExpression:subexpressions() + return {self.expression} +end + +--- @param subexpressions table<number, Expression> +--- @return DiffExpression +function DiffExpression:setsubexpressions(subexpressions) + return DiffExpression(subexpressions[1], self.symbols) +end + +--- @param other Expression +--- @return boolean +function DiffExpression:order(other) + if other:type() == IntegralExpression then + return true + end + + if other:type() ~= DiffExpression then + return false + end + + if self.degree > other.degree then + return false + end + + if self.degree < other.degree then + return true + end + + return self.expression:order(other.expression) +end + +--- @return string +function DiffExpression:tolatex() + local varlist = '\\frac{' + if self.degree == 1 then + varlist = varlist .. 'd}{d' .. self.symbols[1]:tolatex() .. '}\\left(' .. self.expression:tolatex() .. '\\right)' + end + if self.degree > 1 then + local cvarlist = {} + local count = 1 + for index, var in ipairs(self.symbols) do + if var == self.symbols[index+1] then + count = count + 1 + else + table.insert(cvarlist,{var,count}) + count = 1 + end + end + if #cvarlist == 1 then + varlist = varlist .. 'd^{' .. self.degree .. '}}{' .. 'd' .. cvarlist[1][1]:tolatex() .. '^{' .. self.degree .. '}' + end + if #cvarlist > 1 then + varlist = varlist .. '\\partial^{' .. self.degree .. '}}{' + for index, varnum in ipairs(cvarlist) do + var = cvarlist[#cvarlist - index+1][1] + num = cvarlist[#cvarlist - index+1][2] + if num == 1 then + varlist = varlist .. '\\partial ' .. var:tolatex() + else + varlist = varlist .. '\\partial ' .. var:tolatex() .. '^{' .. num .. '}' + end + end + end + varlist = varlist .. '} \\left(' .. self.expression:tolatex() .. '\\right)' + end + return varlist +end + +----------------- +-- Inheritance -- +----------------- + +__DiffExpression.__index = CompoundExpression +__DiffExpression.__call = DiffExpression.new +DiffExpression = setmetatable(DiffExpression, __DiffExpression) + +---------------------- +-- Static constants -- +---------------------- + +diff = function(expression,...) + local symbols = {} + for i = 1, select("#",...) do + local var = select(i,...) + if #var == 0 then + table.insert(symbols,var) + end + if #var > 0 then + for index=1, RR(var[2]) do + table.insert(symbols,var[1]) + end + end + end + return DiffExpression(expression,symbols) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua b/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua new file mode 100644 index 0000000000..5267127264 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua @@ -0,0 +1,934 @@ +--- @class IntegralExpression +--- An expression for the integral of an expression. +--- @field symbol SymbolExpression +--- @field expression Expression +--- @field upper Expression +--- @field lower Expression +--- @field attempts table<number, Expression> +--- @field results table<number, Expression> +--- @field enhancedsubstitution Integer +--- @field recursive boolean + +IntegralExpression = {} +__IntegralExpression = {} + + +-------------------------- +-- Static functionality -- +-------------------------- + +--- Recursive part of the indefinite integral operator. Returns nil if the expression could not be integrated. +--- We switch to prodcedural programming here because it is more natural. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.integrate(integral) + integral.expression = integral.expression:autosimplify() + + if not integral.recursive and #integral.attempts > 0 then + return Copy(integral:lock(Expression.NIL, true)) + end + + -- print(integral.expression) + + local F = IntegralExpression.table(integral) + if F then return F end + + -- If we see the same integrand again, and hasn't been solved already, then the integral can't be solved + local resultindex = Contains(integral.attempts, integral.expression) + if resultindex then + return integral.results[resultindex] + end + local newindex = #integral.attempts+1 + integral.attempts[newindex] = integral.expression + + -- print("Evalutaing: " .. tostring(integral.expression)) + + F = IntegralExpression.linearproperties(integral) + if F then + -- print("Linear Properties") + integral.results[newindex] = F + return F + end + + -- local exp = integral.expression + -- local sym = integral.symbol + -- local es = integral.enhancedsubstitution + -- integral.enhancedsubstitution = Integer.zero() + F = IntegralExpression.substitutionmethod(integral) + if F then + -- print("u-Substitution") + integral.results[newindex] = F + return F + end + -- integral.expression = exp + -- integral.symbol = sym + -- integral.enhancedsubstitution = es + + + F = IntegralExpression.rationalfunction(integral) + if F then + -- print("Rational Function") + integral.results[newindex] = F + return F + end + + F = IntegralExpression.partsmethod(integral) + if F then + -- print("Parts") + integral.results[newindex] = F + return F + end + + F = IntegralExpression.eulersformula(integral) + if F then + -- print("Euler's formula") + integral.results[newindex] = F + return F + end + + local expanded = integral.expression:expand() + if integral.expression ~= expanded then + integral.expression = expanded + F = IntegralExpression.integrate(integral) + if F then + -- print("Expanded") + integral.results[newindex] = F + return F + end + end + + expanded = (Integer.one()/((Integer.one()/integral.expression):autosimplify():expand())):autosimplify() + if integral.expression ~= expanded then + integral.expression = expanded + F = IntegralExpression.integrate(integral) + if F then + -- print("Inverse Expanded") + integral.results[newindex] = F + return F + end + end + + F = IntegralExpression.enhancedsubstitutionmethod(integral) + if F then + -- print("Enhanced u-Substitution") + integral.results[newindex] = F + return F + end + + return nil +end + +--- A table of basic integrals, returns nil if the integrand isn't in the table. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.table(integral) + local integrand = integral.expression + local symbol = integral.symbol + + -- Constant integrand rule - int(c, x) = c*x + if integrand:freeof(symbol) then + return integrand*symbol + end + + if integrand:type() == SymbolExpression then + + -- int(x, x) = x^2/2 + if integrand == symbol then + return integrand ^ Integer(2) / Integer(2) + end + + -- Constant integrand rule again + return integrand*symbol + end + + if integrand:type() == BinaryOperation then + + if integrand.operation == BinaryOperation.POW then + -- int(1/x, x) = ln(x) + if integrand.expressions[1] == symbol and integrand.expressions[2] == Integer(-1) then + return LN(symbol) + end + + -- Cavalieri's formula - int(x^n, x) = x^(n+1)/(n+1) + if integrand.expressions[1] == symbol and integrand.expressions[2]:freeof(symbol) then + return symbol ^ (integrand.expressions[2] + Integer.one()) / (integrand.expressions[2] + Integer.one()) + end + + -- int(n^x, x) = n^x/ln(n) + if integrand.expressions[1]:freeof(symbol) and integrand.expressions[2] == symbol then + return integrand / LN(integrand.expressions[1]) + end + + -- int(csc(x)^2, x) = -cot(x) + if integrand.expressions[1] == CSC(symbol) and integrand.expressions[2] == Integer(2) then + return -COT(symbol) + end + + -- int(sec(x)^2, x) = tan(x) + if integrand.expressions[1] == SEC(symbol) and integrand.expressions[2] == Integer(2) then + return TAN(symbol) + end + end + + if integrand.operation == BinaryOperation.MUL and #integrand.expressions == 2 then + -- int(tan(x)sec(x), x) = sec(x) + if integrand.expressions[1] == TAN(symbol) and integrand.expressions[2] == SEC(symbol) then + return SEC(symbol) + end + + -- int(csc(x)cot(x), x) = -csc(x) + if integrand.expressions[1] == CSC(symbol) and integrand.expressions[2] == COT(symbol) then + return -CSC(symbol) + end + end + + return nil + end + + if integrand:type() == Logarithm then + -- int(log_n(x), x) = (x*ln(x)-x)/ln(n) + if integrand.base:freeof(symbol) and integrand.expression == symbol then + return (symbol * LN(symbol) - symbol) / LN(integrand.base) + end + + return nil + end + + if integrand:type() == TrigExpression then + if integrand == SIN(symbol) then + return -COS(symbol) + end + + if integrand == COS(symbol) then + return SIN(symbol) + end + + if integrand == TAN(symbol) then + return -LN(COS(symbol)) + end + + if integrand == CSC(symbol) then + return -LN(CSC(symbol)+COT(symbol)) + end + + if integrand == SEC(symbol) then + return LN(SEC(symbol) + TAN(symbol)) + end + + if integrand == COT(symbol) then + return LN(SIN(symbol)) + end + + if integrand == ARCSIN(symbol) then + return symbol*ARCSIN(symbol) + (Integer.one()-symbol^(Integer(2)))^(Integer.one()/Integer(2)) + end + + if integrand == ARCCOS(symbol) then + return symbol*ARCCOS(symbol) - (Integer.one()-symbol^(Integer(2)))^(Integer.one()/Integer(2)) + end + + if integrand == ARCTAN(symbol) then + return symbol*ARCTAN(symbol) - (Integer.one()/Integer(2))*LN(Integer.one()+symbol^Integer(2)) + end + + if integrand == ARCCSC(symbol) then + return symbol*ARCCSC(symbol) + LN(symbol*(Integer.one()+(Integer.one()-symbol^(Integer(-2)))^(Integer.one()/Integer(2)))) + end + + if integrand == ARCSEC(symbol) then + return symbol*ARCSEC(symbol) - LN(symbol*(Integer.one()+(Integer.one()-symbol^(Integer(-2)))^(Integer.one()/Integer(2)))) + end + + if integrand == ARCCOT(symbol) then + return symbol*ARCCOT(symbol) + (Integer.one()/Integer(2))*LN(Integer.one()+symbol^Integer(2)) + end + end + + return nil +end + +--- Uses linearity to break up the integral and integrate each piece. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.linearproperties(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + if expression:type() == BinaryOperation then + if expression.operation == BinaryOperation.MUL then + local freepart = Integer.one() + local variablepart = Integer.one() + for _, term in ipairs(expression.expressions) do + if term:freeof(symbol) then + freepart = freepart*term + else + variablepart = variablepart*term + end + end + if freepart == Integer.one() then + return nil + end + integral.expression = variablepart + integral.symbol = symbol + integral.enhancedsubstitution = es + local F = IntegralExpression.integrate(integral) + if F then + return freepart*F + end + return nil + end + + if expression.operation == BinaryOperation.ADD then + local sum = Integer.zero() + for _, term in ipairs(expression.expressions) do + integral.expression = term + integral.symbol = symbol + integral.enhancedsubstitution = es + local F = IntegralExpression.integrate(integral) + if F then + sum = sum + F + else + return nil + end + + end + return sum + end + end + + return nil +end + +--- Attempts u-substitutions to evaluate the integral. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.substitutionmethod(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + local P = IntegralExpression.trialsubstitutions(expression) + local F = nil + local i = 1 + + while not F and i <= #P do + local g = P[i] + if g ~= symbol and not g:freeof(symbol) then + local subsymbol = SymbolExpression("u") + if symbol == SymbolExpression("u") then + subsymbol = SymbolExpression("v") + end + local u = (expression / (DerivativeExpression(g, symbol))):autosimplify() + u = u:substitute({[g]=subsymbol}):autosimplify() + + --factor u and cancel like non-constant terms + u = u:factor():autosimplify() + + if u:freeof(symbol) then + integral.expression = u + integral.symbol = subsymbol + integral.enhancedsubstitution = es + F = IntegralExpression.integrate(integral) + if F then + if integral.recursive then + F = F:substitute({[subsymbol]=g}) + end + return F + end + end + end + i = i + 1 + end + + return F +end + +--- Attempts u-substitutions to evaluate the integral, including solving for the original variable and substituting the result into the expression. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.enhancedsubstitutionmethod(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + local P = IntegralExpression.trialsubstitutions(expression) + local F = nil + local i = 1 + + while not F and i <= #P do + local g = P[i] + if g ~= symbol and not g:freeof(symbol) then + local subsymbol = SymbolExpression("u") + if symbol == SymbolExpression("u") then + subsymbol = SymbolExpression("v") + end + local u = (expression / (DerivativeExpression(g, symbol))):autosimplify() + u = u:substitute({[g]=subsymbol}):autosimplify() + + --factor u and cancel like non-constant terms + u = u:factor():autosimplify() + + if integral.enhancedsubstitution > Integer.zero() then + local f = Equation(subsymbol, g):solvefor(symbol) + if f.lhs == symbol then + u = u:substitute({[symbol]=f.rhs}):autosimplify() + integral.expression = u + integral.symbol = subsymbol + integral.enhancedsubstitution = integral.enhancedsubstitution - Integer.one() + F = IntegralExpression.integrate(integral) + if F then + if integral.recursive then + F = F:substitute({[subsymbol]=g}) + end + return F + end + integral.enhancedsubstitution = integral.enhancedsubstitution + Integer.one() + end + end + end + i = i + 1 + end + + return F +end + +--- Generates a list of possible u-substitutions to attempt +--- @param expression Expression +--- @return table<number, Expression> +function IntegralExpression.trialsubstitutions(expression) + local substitutions = {} + + -- Recursive part - evaluates each term in a product. + if expression:type() == BinaryOperation and expression.operation == BinaryOperation.MUL then + substitutions[#substitutions+1] = expression + for _, term in ipairs(expression.expressions) do + substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(term)) + end + end + + --Recursive part - evaluates each term in a sum. + if expression:type() == BinaryOperation and expression.operation == BinaryOperation.ADD then + substitutions[#substitutions+1] = expression + for _,term in ipairs(expression.expressions) do + substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(term)) + end + end + + -- Function forms and arguments of function forms (includes a recursive part) + if expression:type() == TrigExpression or expression:type() == Logarithm then + substitutions[#substitutions+1] = expression + if not expression.expression:isatomic() then + substitutions[#substitutions+1] = expression.expression + end + substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expression)) + end + + -- Bases and exponents of powers + if expression:type() == BinaryOperation and expression.operation == BinaryOperation.POW then + substitutions[#substitutions+1] = expression + -- Atomic expressions are technically valid substitutions, but they won't be useful + if not expression.expressions[1]:isatomic() then + --substitutions[#substitutions+1] = expression.expressions[1] + substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expressions[1])) + end + if not expression.expressions[2]:isatomic() then + --substitutions[#substitutions+1] = expression.expressions[2] + substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expressions[2])) + end + end + + return substitutions +end + + +--- Uses Lazard, Rioboo, Rothstein, and Trager's method to integrate rational functions. +--- This is mostly to try to avoid factoring and finding the roots of the full denominator whenever possible. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.rationalfunction(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + -- Type checking and conversion to polynomial type. + local f, g, fstat, gstat + if expression:type() == BinaryOperation and expression.operation == BinaryOperation.POW and expression.expressions[2] == Integer(-1) then + g, gstat = expression.expressions[1]:topolynomial() + if not gstat then + return nil + end + f = PolynomialRing({Integer.one()}, g.symbol) + else + if expression:type() ~= BinaryOperation or expression.operation ~= BinaryOperation.MUL or expression.expressions[3] then + return nil + end + if expression.expressions[2]:type() == BinaryOperation and expression.expressions[2].operation == BinaryOperation.POW and expression.expressions[2].expressions[2] == Integer(-1) then + if expression.expressions[1].topolynomial ~=nil and expression.expressions[2].expressions[1].topolynomial ~=nil then + f, fstat = expression.expressions[1]:topolynomial() + g, gstat = expression.expressions[2].expressions[1]:topolynomial() + end + elseif expression.expressions[1]:type() == BinaryOperation and expression.expressions[1].operation == BinaryOperation.POW and expression.expressions[1].expressions[2] == Integer(-1) then + if expression.expressions[2].topolynomial ~= nil and expression.expressions[1].expressions[1].topolynomial ~= nil then + f, fstat = expression.expressions[2]:topolynomial() + g, gstat = expression.expressions[1].expressions[1]:topolynomial() + end + else + return nil + end + + if not fstat or not gstat or f.symbol ~= symbol.symbol or g.symbol ~= symbol.symbol then + return nil + end + end + + -- Explicit handling of degree 1 or less over a binomial. + do + local disc = g.coefficients[1]*g.coefficients[1]-Integer(4)*g.coefficients[2]*g.coefficients[0] + if f.degree <= Integer.one() and g.degree == Integer(2) and disc < Integer.zero() then + return (f.coefficients[1] * LN(g.coefficients[0] + g.coefficients[1] * symbol + g.coefficients[2] * symbol ^ Integer(2))/(Integer(2) * g.coefficients[2]) + (Integer(2)*f.coefficients[0]*g.coefficients[2] - f.coefficients[1]*g.coefficients[1]) / (g.coefficients[2] * sqrt(Integer(4)*g.coefficients[0]*g.coefficients[2] - g.coefficients[1] ^ Integer(2))) * ARCTAN((Integer(2)*g.coefficients[2]*symbol+g.coefficients[1]) / sqrt(Integer(4)*g.coefficients[0]*g.coefficients[2]-g.coefficients[1] ^ Integer(2)))):autosimplify() + end + end + + -- If the polynomials are not relatively prime, divides out the common factors. + local gcd = PolynomialRing.gcd(f, g) + if gcd ~= Integer.one() then + f, g = f // gcd, g // gcd + end + + -- Seperates out the polynomial part and rational part and integrates the polynomial part. + local q, h = f:divremainder(g) + integral.expression = q + integral.symbol = symbol + integral.enhancedsubstitution = es + U = IntegralExpression.integrate(integral) + + if h == Integer.zero() then + return U + end + + -- Performs partial fraction decomposition into square-free denominators on the rational part. + local gg = g:squarefreefactorization() + local pfd = PolynomialRing.partialfractions(h, g, gg) + + -- Hermite reduction. + local V = Integer.zero() + for _, term in ipairs(pfd.expressions) do + local i = #term.expressions + if i > 1 then + for j = 1, i-1 do + local n = term.expressions[j].expressions[1] + local d = term.expressions[j].expressions[2].expressions[1] + local p = term.expressions[j].expressions[2].expressions[2] + + local _, s, t = PolynomialRing.extendedgcd(d, d:derivative()) + s = s * n + t = t * n + V = V - t / ((p-Integer.one()) * BinaryOperation.POWEXP({d, p-Integer.one()})) + term.expressions[j+1].expressions[1] = term.expressions[j+1].expressions[1] + s + t:derivative() / (p-Integer.one()) + end + end + end + + --Lazard-Rioboo-Trager method. + local W = Integer.zero() + for _, term in ipairs(pfd.expressions) do + local a = term.expressions[#term.expressions].expressions[1] + local b = term.expressions[1].expressions[2].expressions[1] + local y = a - b:derivative() * PolynomialRing({Integer.zero(), Integer.one()}, "_") + local r = PolynomialRing.resultant(b, y) + + + local rr = r:squarefreefactorization() + local remainders = PolynomialRing.monicgcdremainders(b, y) + for pos, factor in ipairs(rr.expressions) do + if pos > 1 then + local re = factor.expressions[1] + local e = factor.expressions[2] + local roots = re:roots() + for _, root in ipairs(roots) do + local w + for _, remainder in ipairs(remainders) do + if remainder.degree == e then + w = remainder + break + end + end + W = W + root*LN(w:substitute({[SymbolExpression("_")] = root})) + end + end + end + end + + return U + V + W +end + + +--- Attempts integration by parts for expressions with a polynomial factor in them. Other product expressions use Euler's formula. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.partsmethod(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + if expression:type() ~= BinaryOperation or expression.operation ~= BinaryOperation.MUL then + return + end + + local u + local vp = Integer.one() + --looking for ILATE + for _, exp in ipairs(expression:subexpressions()) do + if exp:type() == TrigExpression and (exp.name == "arctan" or exp.name == "arccos" or exp.name == "arcsin" or exp.name == "arccot" or exp.name == "arcsec" or exp.name == "arccsc") then + u = exp + else + vp = vp * exp + end + end + + if not u or u:freeof(symbol) then + goto skipI + else + vp = vp:autosimplify() + end + + --if vp:type() == Logarithm or vp.topolynomial or (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then + if select(2,vp:topolynomial()) then + integral.expression = vp + integral.symbol = symbol + integral.enhancedsubstitution = es + local v = IntegralExpression.integrate(integral) + if not v then + goto skipI + end + + local up = DerivativeExpression(u, symbol):autosimplify() + + integral.expression = v*up + integral.symbol = symbol + integral.enhancedsubstitution = es + local vup = IntegralExpression.integrate(integral) + if not vup then + goto skipI + end + + local result = u*v - vup + + return result:autosimplify() + end + ::skipI:: + + local u + local vp = Integer.one() + --looking for LATE + for _, exp in ipairs(expression:subexpressions()) do + if exp:type() == Logarithm then + u = exp + else + vp = vp * exp + end + end + + if not u or u:freeof(symbol) then + goto skipL + else + vp = vp:autosimplify() + end + + --if vp.topolynomial or (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then + if select(2,vp:topolynomial()) then + integral.expression = vp + integral.symbol = symbol + integral.enhancedsubstitution = es + local v = IntegralExpression.integrate(integral) + if not v then + goto skipL + end + + local up = DerivativeExpression(u, symbol):autosimplify() + + integral.expression = v*up + integral.symbol = symbol + integral.enhancedsubstitution = es + local vup = IntegralExpression.integrate(integral) + if not vup then + goto skipL + end + + local result = u*v - vup + + return result:autosimplify() + end + ::skipL:: + + local u + local vp = Integer.one() + --looking for ATE + for _, exp in ipairs(expression:subexpressions()) do + local _, bool = exp:topolynomial() + if bool then + u = exp + else + vp = vp * exp + end + end + + if not u or u:freeof(symbol) then + return + else + vp = vp:autosimplify() + end + + if (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then + local results = {} + while u ~= Integer.zero() do + integral.expression = vp + integral.symbol = symbol + integral.enhancedsubstitution = es + local v = IntegralExpression.integrate(integral):unlock():autosimplify() + if not v then + return + end + local up = DerivativeExpression(u, symbol):autosimplify() + + if not integral.recursive then + return (u*v - IntegralExpression(v*up, symbol):nonrecursive():lock(Expression.NIL, true)):autosimplify() + end + + results[#results+1] = u*v + u = up + vp = v + end + + local result = results[#results] + for i=#results-1,1,-1 do + result = results[i] - result + end + + return result:autosimplify() + end +end + +--- Attempts integration using Euler's formula and kind. Alternative for integration by parts for many expressions. +--- @param integral IntegralExpression +--- @return Expression|nil +function IntegralExpression.eulersformula(integral) + local expression = integral.expression + local symbol = integral.symbol + local es = integral.enhancedsubstitution + + local new = expression:substitute({[COS(symbol)] = (E^(I*symbol) + E^(-I*symbol))/Integer(2), + [SIN(symbol)] = (E^(I*symbol) - E^(-I*symbol))/(Integer(2)*I)}) + + if new == expression then + return + end + + integral.expression = new:autosimplify():expand() + integral.symbol = symbol + integral.enhancedsubstitution = es + local complexresult = IntegralExpression.integrate(integral) + if not complexresult then + return + end + + -- TODO: Proper complex number conversion methods + local function converttorectangular(exp) + exp = exp:expand() + local results = {} + for index, sub in ipairs(exp:subexpressions()) do + results[index] = converttorectangular(sub) + end + local converted = exp:setsubexpressions(results) + + if converted.operation == BinaryOperation.POW and converted.expressions[1] == E and converted.expressions[2].operation == BinaryOperation.MUL then + local ipart + local rest = Integer.one() + for _, factor in ipairs(converted.expressions[2]:subexpressions()) do + if factor == I then + ipart = true + else + rest = rest * factor + end + end + if ipart then + return (COS(rest) + I*SIN(rest)):autosimplify() + end + end + + return converted + end + + return converttorectangular(complexresult:autosimplify()):expand():autosimplify() + +end + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new integral operation with the given symbol and expression. +--- @param expression Expression +--- @param symbol SymbolExpression +--- @param lower Expression +--- @param upper Expression +function IntegralExpression:new(expression, symbol, lower, upper) + local o = {} + local __o = Copy(__ExpressionOperations) + + if not symbol or not expression then + error("Send wrong number of parameters: integrals must have a variable to integrate with respect to and an expression to integrate.") + end + + if lower and not upper then + error("Send wrong number of parameters: definite integrals must have an upper and a lower bound.") + end + + o.symbol = symbol + o.expression = Copy(expression) + o.upper = Copy(upper) + o.lower = Copy(lower) + o.recursive = true + + o.attempts = {} + o.results = {} + o.enhancedsubstitution = IntegralExpression.ENHANCEDSUBSTITUTIONRECURSIONLIMIT + + __o.__index = IntegralExpression + __o.__tostring = function(a) + if a:isdefinite() then + return 'int(' .. tostring(a.expression) .. ", " .. tostring(a.symbol) .. ", ".. tostring(a.lower) .. ', ' .. tostring(a.upper) .. ')' + end + return 'int(' .. tostring(a.expression) .. ", " .. tostring(a.symbol) .. ')' + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway + if not b:type() == IntegralExpression then + return false + end + return a.symbol == b.symbol and a.expression == b.expression and a.upper == b.upper and a.lower == b.lower + end + o = setmetatable(o, __o) + + return o +end + +--- Returns true if the integral is definite, i.e., has an upper and lower bound. +--- @return boolean +function IntegralExpression:isdefinite() + return self.upper ~= nil +end + +--- Sets the integral to not autosimplify other integral expressions that are produced by the integration process. +--- THIS METHOD MUTATES THE OBJECT IT IS CALLED ON. +function IntegralExpression:nonrecursive() + self.recursive = false + return self +end + +--- @return Expression +function IntegralExpression:autosimplify() + local arg = IntegralExpression(self.expression, self.symbol) + local integrated = IntegralExpression.integrate(arg) + + -- Our expression could not be integrated. + if not integrated then + return self + end + + if not self.recursive then + return integrated:autosimplify():unlock(true) + end + + if self:isdefinite() then + return (integrated:substitute({[self.symbol]=self.upper}) - integrated:substitute({[self.symbol]=self.lower})):autosimplify() + end + + return integrated:autosimplify() +end + + +--- @return table<number, Expression> +function IntegralExpression:subexpressions() + if self:isdefinite() then + return {self.expression, self.symbol, self.lower, self.upper} + end + + return {self.expression, self.symbol} +end + +--- @param subexpressions table<number, Expression> +--- @return IntegralExpression +function IntegralExpression:setsubexpressions(subexpressions) + local out = IntegralExpression(subexpressions[1], subexpressions[2], subexpressions[3], subexpressions[4]) + + return out; +end + +-- function IntegralExpression:freeof(symbol) +-- if self:isdefinite() then +-- return self.expression:freeof(symbol) and self.upper:freeof(symbol) and self.lower:freeof(symbol) +-- end +-- return self.expression:freeof(symbol) +-- end + +-- -- Substitutes each expression for a new one. +-- function IntegralExpression:substitute(map) +-- for expression, replacement in pairs(map) do +-- if self == expression then +-- return replacement +-- end +-- end +-- -- Typically, we only perform substitution on autosimplified expressions, so this won't get called. May give strange results, i.e., +-- -- substituting and then evaluating the integral may not return the same thing as evaluating the integral and then substituting. +-- if self:isdefinite() then +-- return IntegralExpression(self.symbol, self.expression:substitute(map), self.upper:substitute(map), self.lower:substitute(map)) +-- end +-- return IntegralExpression(self.symbol, self.expression:substitute(map)) +-- end + +--- @param other Expression +--- @return boolean +function IntegralExpression:order(other) + if other:type() ~= IntegralExpression then + return false + end + + if self.symbol ~= other.symbol then + return self.symbol:order(other.symbol) + end + + return self.expression:order(other.expression) +end + +--- @return string +function IntegralExpression:tolatex() + if self:isdefinite() then + return '\\int_{' .. self.lower:tolatex() .. '}^{' .. self.upper:tolatex() .. '}{' .. self.expression:tolatex() .. '\\mathop{d' .. self.symbol:tolatex() .. '}}' + end + return '\\int{' .. self.expression:tolatex() .. '\\mathop{d' .. self.symbol:tolatex() .. '}}' +end + + +----------------- +-- Inheritance -- +----------------- +__IntegralExpression.__index = CompoundExpression +__IntegralExpression.__call = IntegralExpression.new +IntegralExpression = setmetatable(IntegralExpression, __IntegralExpression) + +---------------------- +-- Static constants -- +---------------------- +INT = function(symbol, expression, lower, upper) + return IntegralExpression(symbol, expression, lower, upper) +end + +---------------------- +-- Static constants -- +---------------------- + +-- Limit for the maximum number of full u-subs to attempts for any integral. +-- This should be low, since integrals are highly unlikely to need more than 1 or 2 u-subs, and gives exponentially worse performance the higher the number is. +IntegralExpression.ENHANCEDSUBSTITUTIONRECURSIONLIMIT = Integer(2)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/_init.lua b/macros/luatex/latex/luacas/tex/core/_init.lua new file mode 100644 index 0000000000..cfd640a214 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/_init.lua @@ -0,0 +1,16 @@ +-- Loads core files in the correct order. + +require("core.expression") +require("core.atomicexpression") +require("core.compoundexpression") +require("core.constantexpression") +require("core.symbolexpression") +require("core.binaryoperation") +require("core.functionexpression") + + +require("core.binaryoperation.power") +require("core.binaryoperation.product") +require("core.binaryoperation.sum") +require("core.binaryoperation.quotient") +require("core.binaryoperation.difference")
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/atomicexpression.lua b/macros/luatex/latex/luacas/tex/core/atomicexpression.lua new file mode 100644 index 0000000000..6cc6e4f58e --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/atomicexpression.lua @@ -0,0 +1,70 @@ +--- @class AtomicExpression +--- Interface for an atomic mathematical expression that has no sub-expressions. +AtomicExpression = {} +__AtomicExpression = {} + + +---------------------- +-- Required methods -- +---------------------- + +--- Converts an atomic expression to its equivalent compound expression, if it has one. +--- @return Expression +function AtomicExpression:tocompoundexpression() + return self +end + +---------------------- +-- Instance methods -- +---------------------- + +--- @return AtomicExpression +function AtomicExpression:evaluate() + return self +end + +--- @return AtomicExpression +function AtomicExpression:autosimplify() + return self +end + +--- @return table<number, Expression> +function AtomicExpression:subexpressions() + return {} +end + +--- @param subexpressions table<number, Expression> +--- @return AtomicExpression +function AtomicExpression:setsubexpressions(subexpressions) + return self +end + +--- @param map table<Expression, Expression> +--- @return Expression +function AtomicExpression:substitute(map) + for expression, replacement in pairs(map) do + if self == expression then + return replacement + end + end + return self +end + +--- @return boolean +function AtomicExpression:isatomic() + return true +end + +--- @return string +function AtomicExpression:tolatex() + -- Most atomic expressions should have the same __tostring as LaTeX's output + return tostring(self) +end + + +----------------- +-- Inheritance -- +----------------- + +__AtomicExpression.__index = Expression +AtomicExpression = setmetatable(AtomicExpression, __AtomicExpression)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation.lua new file mode 100644 index 0000000000..708a6c1540 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation.lua @@ -0,0 +1,800 @@ +--- @class BinaryOperation +--- Represents a binary operation with two inputs and one output. +--- Represents a generic function that takes zero or more expressions as inputs. +--- @field name string +--- @field operation function +--- @field expressions table<number, Expression> +BinaryOperation = {} +__BinaryOperation = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new binary operation with the given operation. +--- @param operation function +--- @param expressions table<number, Expression> +--- @return BinaryOperation +function BinaryOperation:new(operation, expressions) + local o = {} + local __o = Copy(__ExpressionOperations) + + if type(operation) ~= "function" then + error("Sent parameter of wrong type: operation must be a function") + end + + if type(expressions) ~= "table" then + error("Sent parameter of wrong type: expressions must be an array") + end + + o.name = BinaryOperation.DEFAULT_NAMES[operation] + o.operation = operation + o.expressions = Copy(expressions) + + if BinaryOperation.COMMUTATIVITY[operation] then + function o:iscommutative() + return true + end + else + function o:iscommutative() + return false + end + end + + if not o:iscommutative() and o.operation ~= BinaryOperation.SUB and #o.expressions ~= 2 then + error("Sent parameter of wrong type: noncommutative operations cannot have an arbitrary number of paramaters") + end + + __o.__index = BinaryOperation + __o.__tostring = function(a) + local expressionnames = '' + for index, expression in ipairs(a.expressions) do + if index == 1 and not a.expressions[index + 1] then + expressionnames = expressionnames .. a.name .. ' ' + end + if index > 1 then + expressionnames = expressionnames .. ' ' + end + if expression:isatomic() and not (a.operation == BinaryOperation.POW and expression:type() == Rational) then + expressionnames = expressionnames .. tostring(expression) + else + expressionnames = expressionnames .. '(' .. tostring(expression) .. ')' + end + if a.expressions[index + 1] then + expressionnames = expressionnames .. ' ' .. a.name + end + end + return expressionnames + end + __o.__eq = function(a, b) + -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway + if not a.operation or not b.operation then + return false + end + local loc = 1 + while a.expressions[loc] or b.expressions[loc] do + if not a.expressions[loc] or not b.expressions[loc] or + (a.expressions[loc] ~= b.expressions[loc]) then + return false + end + loc = loc + 1 + end + return a.operation == b.operation + end + o = setmetatable(o, __o) + + return o +end + +--- @return Expression +function BinaryOperation:evaluate() + local results = {} + local reducible = true + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:evaluate() + if not results[index]:isconstant() then + reducible = false + end + end + if not reducible then + return BinaryOperation(self.operation, results) + end + + if not self.expressions[1] then + error("Execution error: cannot perform binary operation on zero expressions") + end + + local result = results[1] + for index, expression in ipairs(results) do + if not (index == 1) then + result = self.operation(result, expression) + end + end + return result +end + +--- @return Expression +function BinaryOperation:autosimplify() + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:autosimplify() + end + local simplified = BinaryOperation(self.operation, results) + if simplified.operation == BinaryOperation.POW then + return simplified:simplifypower() + end + if simplified.operation == BinaryOperation.MUL then + return simplified:simplifyproduct() + end + if simplified.operation == BinaryOperation.ADD then + return simplified:simplifysum() + end + if simplified.operation == BinaryOperation.DIV then + return simplified:simplifyquotient() + end + if simplified.operation == BinaryOperation.SUB then + return simplified:simplifydifference() + end + return simplified +end + +--- @return table<number, Expression> +function BinaryOperation:subexpressions() + return self.expressions +end + +--- @param subexpressions table<number, Expression> +--- @return BinaryOperation +function BinaryOperation:setsubexpressions(subexpressions) + return BinaryOperation(self.operation, subexpressions) +end + +--- @return Expression +function BinaryOperation:expand() + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:expand() + end + local expanded = BinaryOperation(self.operation, results) + if expanded.operation == BinaryOperation.MUL then + local allsums = BinaryOperation(BinaryOperation.ADD, {Integer.one()}) + for _, expression in ipairs(expanded.expressions) do + allsums = allsums:expand2(expression) + end + return allsums:autosimplify() + end + if expanded.operation == BinaryOperation.POW and expanded.expressions[2]:type() == Integer then + if expanded.expressions[1]:type() ~= BinaryOperation then + return expanded:autosimplify() + end + local exp = BinaryOperation.MULEXP({Integer.one()}) + local pow = expanded.expressions[2]:asnumber() + for _ = 1, math.abs(pow) do + exp = exp:expand2(expanded.expressions[1]) + if _ > 1 then + exp = exp:autosimplify() + end + end + if pow < 0 then + exp = exp^Integer(-1) + end + return exp + end + if expanded.operation == BinaryOperation.POW and expanded.expressions[2].operation == BinaryOperation.ADD then + local exp = {} + for i = 1, #expanded.expressions[2].expressions do + exp[#exp+1] = (expanded.expressions[1]^expanded.expressions[2].expressions[i]):autosimplify() + end + return BinaryOperation.MULEXP(exp) + end + return expanded:autosimplify() +end + +--- Helper for expand - multiplies two addition expressions. +--- @return Expression +function BinaryOperation:expand2(other) + local result = {} + for _, expression in ipairs(self:subexpressions()) do + if other:type() == BinaryOperation and other.operation == BinaryOperation.ADD then + for _, expression2 in ipairs(other.expressions) do + result[#result+1] = expression * expression2 + end + else + result[#result+1] = expression * other + end + end + return BinaryOperation(BinaryOperation.ADD, result) +end + +--- @return Expression +function BinaryOperation:factor() + local results = {} + + -- Recursively factors sub-expressions + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:factor() + end + + -- Attempts to factor expressions as monovariate polynomials + local factoredsubs = BinaryOperation(self.operation, results) + local subs = factoredsubs:getsubexpressionsrec() + for index, sub in ipairs(subs) do + local substituted = factoredsubs:substitute({[sub]=SymbolExpression("_")}):autosimplify() + local polynomial, result = substituted:topolynomial() + if result then + local factored = polynomial:factor():autosimplify() + if factored ~= substituted then + return factored:substitute({[SymbolExpression("_")]=sub}) + end + end + end + + -- Pulls common sub-expressions out of sum expressions + if self.operation == BinaryOperation.ADD then + local gcf + for _, expression in ipairs(factoredsubs:subexpressions()) do + if expression.operation ~= BinaryOperation.MUL then + expression = BinaryOperation.MULEXP({expression}) + end + if not gcf then + gcf = expression + else + local newgcf = Integer.one() + for _, gcfterm in ipairs(gcf:subexpressions()) do + local gcfpower = Integer.one() + if gcfterm:type() == BinaryOperation and gcfterm.operation == BinaryOperation.POW and gcfterm.expressions[2]:type() == Integer then + gcfpower = gcfterm.expressions[2] + gcfterm = gcfterm.expressions[1] + end + for _, term in ipairs(expression:subexpressions()) do + local power = Integer.one() + if term:type() == BinaryOperation and term.operation == BinaryOperation.POW and term.expressions[2]:type() == Integer then + power = term.expressions[2] + term = term.expressions[1] + end + if term == gcfterm then + newgcf = newgcf * term^Integer.min(power, gcfpower) + end + end + end + gcf = newgcf + end + end + if gcf:type() ~= Integer then + local out = Integer.zero() + for _, expression in ipairs(factoredsubs:subexpressions()) do + out = out + expression/gcf + end + out = gcf*(out:autosimplify():factor()) + return out:autosimplify() + end + end + + return factoredsubs +end + +--- @return Expression +function BinaryOperation:combine() + local den, num, aux, mul, input = {}, {}, {}, {}, self:autosimplify():expand() + if input.operation ~= BinaryOperation.ADD then + return input + end + for _, expr in ipairs(input.expressions) do + local numpart, denpart = Integer.one(), Integer.one() + if expr.operation == BinaryOperation.POW and expr.expressions[2]:type() == Integer and expr.expressions[2] < Integer.zero() then + denpart = denpart*expr.expressions[1] ^ expr.expressions[2]:neg() + for index,term in ipairs(den) do + if expr.expressions[1] == den[index] then + if expr.expressions[2]:neg() > mul[index] then + mul[index] = expr.expressions[2]:neg() + goto continue + else + goto continue + end + end + end + table.insert(den,expr.expressions[1]) + table.insert(mul,expr.expressions[2]:neg()) + ::continue:: + end + if expr.operation == BinaryOperation.MUL then + for _,subexpr in ipairs(expr.expressions) do + if subexpr.operation == BinaryOperation.POW and subexpr.expressions[2]:type() == Integer and subexpr.expressions[2] < Integer.zero() then + denpart = denpart*subexpr.expressions[1] ^ subexpr.expressions[2]:neg() + for index,term in ipairs(den) do + if subexpr.expressions[1] == den[index] then + if subexpr.expressions[2]:neg() > mul[index] then + mul[index] = subexpr.expressions[2]:neg() + goto continue + else + goto continue + end + end + end + table.insert(den,subexpr.expressions[1]) + table.insert(mul,subexpr.expressions[2]:neg()) + ::continue:: + else + numpart = numpart*subexpr + end + end + end + if expr.operation ~= BinaryOperation.POW and expr.operation ~= BinaryOperation.MUL then + numpart = expr + end + table.insert(num,numpart) + table.insert(aux,denpart) + end + local denominator = Integer.one() + local numerator = Integer.zero() + for index,expr in ipairs(den) do + denominator = denominator*den[index] ^ mul[index] + end + denominator = denominator:autosimplify() + for index,expr in ipairs(num) do + local uncommon = denominator/aux[index] + uncommon = uncommon:factor():simplify() + numerator = numerator + expr*uncommon + end + numerator = numerator:simplify():factor() + if denominator == Integer.one() then + return numerator + else + return numerator/denominator + end +end + +--- @param collect Expression +--- @return Expression +function BinaryOperation:collect(collect) + -- Constant expressions cannot be collected + if collect:isconstant() then + return self + end + + -- Recusively collect subexpressions + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:collect(collect) + end + local collected = BinaryOperation(self.operation, results) + + if not (collected.operation == BinaryOperation.ADD) then + return collected:autosimplify() + end + + local coefficients = {} + + -- TODO: Add an expression map class + setmetatable(coefficients, {__index = + function(table, key) + local out = rawget(table, tostring(key)) + return out or Integer.zero() + end, + __newindex = + function (table, key, value) + rawset(table, tostring(key), value) + end + }) + + -- Finds all instances of a constant power of the expression to be collected, and maps each power to all terms it is multiplied by + for _, expression in ipairs(collected:subexpressions()) do + if expression == collect then + coefficients[Integer.one()] = coefficients[Integer.one()] + Integer.one() + elseif expression.operation == BinaryOperation.POW and expression:subexpressions()[1] == collect and expression:subexpressions()[2]:isconstant() then + coefficients[expression:subexpressions()[2]] = coefficients[expression:subexpressions()[2]] + Integer.one() + elseif collect:type() == BinaryOperation and collect.operation == BinaryOperation.POW and + expression.operation == BinaryOperation.POW and expression:subexpressions()[1] == collect:subexpressions()[1] then + -- Handle the fact that autosimplify turns (a^x^n -> a^(xn)), this is needed if the term to collect is itself an exponential + local power = (expression:subexpressions()[2] / collect:subexpressions()[2]):autosimplify() + if power:isconstant() then + coefficients[power] = coefficients[power] + Integer.one() + else + coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression + end + elseif expression.operation == BinaryOperation.MUL then + local varpart + local coeffpart = Integer.one() + for _, term in ipairs(expression:subexpressions()) do + if term == collect then + varpart = Integer.one() + elseif (term.operation == BinaryOperation.POW and term:subexpressions()[1] == collect and term:subexpressions()[2]:isconstant()) then + varpart = term:subexpressions()[2] + elseif collect:type() == BinaryOperation and collect.operation == BinaryOperation.POW and + term.operation == BinaryOperation.POW and term:subexpressions()[1] == collect:subexpressions()[1] then + local power = (term:subexpressions()[2] / collect:subexpressions()[2]):autosimplify() + if power:isconstant() then + varpart = power + end + else + coeffpart = coeffpart * term + end + end + if varpart then + coefficients[varpart] = coefficients[varpart] + coeffpart + else + coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression + end + else + coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression + end + + + end + + local out = Integer.zero() + for index, value in pairs(coefficients) do + out = out + collect ^ Rational.fromstring(index) * value + end + + return out:autosimplify() +end + +--- @param other Expression +--- @return boolean +function BinaryOperation:order(other) + if other:isconstant() then + return false + end + + if other:isatomic() then + if self.operation == BinaryOperation.POW then + return self:order(BinaryOperation(BinaryOperation.POW, {other, Integer.one()})) + end + + if self.operation == BinaryOperation.MUL then + return self:order(BinaryOperation(BinaryOperation.MUL, {other})) + end + + if self.operation == BinaryOperation.ADD then + return self:order(BinaryOperation(BinaryOperation.ADD, {other})) + end + end + + if self.operation == BinaryOperation.POW and other.operation == BinaryOperation.POW then + if self.expressions[1] ~= other.expressions[1] then + return self.expressions[1]:order(other.expressions[1]) + end + return self.expressions[2]:order(other.expressions[2]) + end + + if (self.operation == BinaryOperation.MUL and other.operation == BinaryOperation.MUL) or + (self.operation == BinaryOperation.ADD and other.operation == BinaryOperation.ADD) then + local k = 0 + while #self.expressions - k > 0 and #other.expressions - k > 0 do + if self.expressions[#self.expressions - k] ~= other.expressions[#other.expressions - k] then + return self.expressions[#self.expressions - k]:order(other.expressions[#other.expressions - k]) + end + k = k + 1 + end + return #self.expressions < #other.expressions + end + + if (self.operation == BinaryOperation.MUL) and (other.operation == BinaryOperation.POW or other.operation == BinaryOperation.ADD) then + return self:order(BinaryOperation(BinaryOperation.MUL, {other})) + end + + if (self.operation == BinaryOperation.POW) and (other.operation == BinaryOperation.MUL) then + return BinaryOperation(BinaryOperation.MUL, {self}):order(other) + end + + if (self.operation == BinaryOperation.POW) and (other.operation == BinaryOperation.ADD) then + return self:order(BinaryOperation(BinaryOperation.POW, {other, Integer.one()})) + end + + if (self.operation == BinaryOperation.ADD) and (other.operation == BinaryOperation.MUL) then + return BinaryOperation(BinaryOperation.MUL, {self}):order(other) + end + + if (self.operation == BinaryOperation.ADD) and (other.operation == BinaryOperation.POW) then + return BinaryOperation(BinaryOperation.POW, {self, Integer.one()}):order(other) + end + + if other:type() == FunctionExpression or other:type() == TrigExpression or other:type() == Logarithm then + if self.operation == BinaryOperation.ADD or self.operation == BinaryOperation.MUL then + return self:order(BinaryOperation(self.operation, {other})) + end + + if self.operation == BinaryOperation.POW then + return self:order(other^Integer.one()) + end + end + + return true +end + +--- Returns whether the binary operation is commutative. +--- @return boolean +function BinaryOperation:iscommutative() + error("Called unimplemented method: iscommutative()") +end + +--- @return PolynomialRing, boolean +function BinaryOperation:topolynomial() + local addexp = self + if not self.operation or self.operation ~= BinaryOperation.ADD then + addexp = BinaryOperation(BinaryOperation.ADD, {self}) + end + + local poly = {} + local degree = 0 + local symbol + for _, expression in ipairs(addexp.expressions) do + local coefficient + local sym + local power + -- Expressions of the form c + if expression:isconstant() then + coefficient = expression + power = 0 + -- Expressions of the form x + elseif expression:type() == SymbolExpression then + coefficient = Integer.one() + sym = expression.symbol + power = 1 + -- Expressions of the form c*x + elseif expression.operation and expression.operation == BinaryOperation.MUL and #expression.expressions == 2 + and expression.expressions[1]:isconstant() and expression.expressions[2]:type() == SymbolExpression then + + coefficient = expression.expressions[1] + sym = expression.expressions[2].symbol + power = 1 + -- Expressions of the form c*x^n (totally not confusing) + elseif expression.operation and expression.operation == BinaryOperation.MUL and #expression.expressions == 2 + and expression.expressions[1]:isconstant() and expression.expressions[2].operation and + expression.expressions[2].operation == BinaryOperation.POW and #expression.expressions[2].expressions == 2 + and expression.expressions[2].expressions[1]:type() == SymbolExpression and expression.expressions[2].expressions[2].getring + and expression.expressions[2].expressions[2]:getring() == Integer.getring() and expression.expressions[2].expressions[2] > Integer.zero() then + + coefficient = expression.expressions[1] + sym = expression.expressions[2].expressions[1].symbol + power = expression.expressions[2].expressions[2]:asnumber() + -- Expressions of the form x^n + elseif expression.operation and expression.operation == BinaryOperation.POW and #expression.expressions == 2 + and expression.expressions[1]:type() == SymbolExpression and expression.expressions[2].getring + and expression.expressions[2]:getring() == Integer.getring() and expression.expressions[2] > Integer.zero() then + + coefficient = Integer.one() + sym = expression.expressions[1].symbol + power = expression.expressions[2]:asnumber() + else + return self, false + end + + if symbol and sym and symbol ~= sym then + return self, false + end + if not symbol then + symbol = sym + end + poly[power + 1] = coefficient + if power > degree then + degree = power + end + end + + for i = 1,degree+1 do + poly[i] = poly[i] or Integer.zero() + end + + return PolynomialRing(poly, symbol), true +end + +function BinaryOperation:tolatex() + if self.operation == BinaryOperation.POW then + if self.expressions[2]:type() == Integer and self.expressions[2] < Integer.zero() then + local base = self.expressions[1] + local exponent = self.expressions[2] + if exponent == Integer(-1) then + return "\\frac{1}{" .. base:tolatex() .. "}" + else + if base:isatomic() then + return "\\frac{1}{" .. base:tolatex() .. "^{" .. exponent:neg():tolatex() .. "}}" + else + return "\\frac{1}{\\left(" .. base:tolatex() .. "\\right)^{" .. exponent:neg():tolatex() .. "}}" + end + end + end + if self.expressions[1]:isatomic() then + if self.expressions[2]:isconstant() and self.expressions[2]:getring() == Rational:getring() and self.expressions[2].numerator == Integer.one() then + if self.expressions[2].denominator == Integer(2) then + return "\\sqrt{" .. self.expressions[1]:tolatex() .. '}' + end + return "\\sqrt[" .. self.expressions[2].denominator:tolatex() .. ']{' .. self.expressions[1]:tolatex() .. '}' + end + return self.expressions[1]:tolatex() .. '^{' .. self.expressions[2]:tolatex() .. '}' + else + if self.expressions[2]:isconstant() and self.expressions[2]:getring() == Rational:getring() and self.expressions[2].numerator == Integer.one() then + if self.expressions[2].denominator == Integer(2) then + return "\\sqrt{" .. self.expressions[1]:tolatex() .. '}' + end + return "\\sqrt[" .. self.expressions[2].denominator:tolatex() .. ']{' .. self.expressions[1]:tolatex() .. '}' + end + return "\\left(" .. self.expressions[1]:tolatex() .. "\\right)" .. '^{' .. self.expressions[2]:tolatex() .. '}' + end + end + if self.operation == BinaryOperation.MUL then + local sign = '' + local out = '' + local denom = '' + if self:autosimplify():isconstant() then + for index, expression in ipairs(self.expressions) do + if index == 1 then + out = out .. expression:tolatex() + else + out = out .. "\\cdot " .. expression:tolatex() + end + end + return out + end + if #self.expressions == 2 and self.expressions[2]:type() == BinaryOperation and self.expressions[2].operation == BinaryOperation.POW and self.expressions[2].expressions[2] == -Integer.one() then + out = '\\frac{' .. self.expressions[1]:tolatex() .. '}{' .. self.expressions[2].expressions[1]:tolatex() .. '}' + return out + end + for _, expression in ipairs(self.expressions) do + if expression:type() == BinaryOperation then + if expression.operation == BinaryOperation.POW and expression.expressions[2]:isconstant() and expression.expressions[2] < Integer.zero() then + local reversed = (Integer.one() / expression):autosimplify() + if reversed.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB then + denom = denom .. '\\left('.. reversed:tolatex() .. '\\right)' + else + denom = denom .. reversed:tolatex() + end + elseif expression.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB then + out = out .. '\\left(' .. expression:tolatex() .. '\\right)' + else + out = out .. expression:tolatex() + end + else + if expression == Integer(-1) then + out = out .. '-' + elseif expression:type() == Rational and expression.numerator == Integer.one() then + denom = denom .. expression.denominator:tolatex() + elseif expression:type() == Rational and expression.numerator == Integer(-1) then + out = out .. '-' + denom = denom .. expression.denominator:tolatex() + elseif expression:type() == Rational then + out = out .. expression.numerator:tolatex() + denom = denom .. expression.denominator:tolatex() + else + out = out .. expression:tolatex() + end + end + end + if string.sub(out,1,1) == '-' then + sign = '-' + out = string.sub(out,2,-1) + end + if denom ~= '' and out == '' then + return sign .. '\\frac{' .. '1' .. '}{' .. denom .. '}' + end + if denom ~= '' then + return sign .. '\\frac{' .. out .. '}{' .. denom .. '}' + end + return sign..out + end + if self.operation == BinaryOperation.ADD then + local out = '' + for index, expression in ipairs(self.expressions) do + out = out .. expression:tolatex() + if self.expressions[index + 1] and string.sub(self.expressions[index + 1]:tolatex(), 1, 1) ~= "-" then + out = out .. '+' + end + end + return out + end + if self.operation == BinaryOperation.DIV then + return '\\frac{' .. self.expressions[1]:tolatex() .. '}{' .. self.expressions[2]:tolatex() .. '}' + end + if self.operation == BinaryOperation.SUB then + local out = '' + if not self.expressions[2] then + if not self.expressions[1]:isatomic() then + out = '-\\left(' .. self.expressions[1]:tolatex() .. '\\right)' + else + out = '-' .. self.expressions[1]:tolatex() + end + else + for index, expression in ipairs(self.expressions) do + if expression.operation and (expression.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB) and index >1 then + out = out .. "\\left(" .. expression:tolatex() .. "\\right)" + else + out = out .. expression:tolatex() + end + if self.expressions[index + 1] then + out = out .. '-' + end + end + end + return out + end + return self +end + +----------------- +-- Inheritance -- +----------------- + +__BinaryOperation.__index = CompoundExpression +__BinaryOperation.__call = BinaryOperation.new +BinaryOperation = setmetatable(BinaryOperation, __BinaryOperation) + +---------------------- +-- Static constants -- +---------------------- + +BinaryOperation.ADD = function(a, b) + return a + b +end + +BinaryOperation.SUB = function(a, b) + return a - b +end + +BinaryOperation.MUL = function(a, b) + return a * b +end + +BinaryOperation.DIV = function(a, b) + return a / b +end + +BinaryOperation.IDIV = function(a, b) + return a // b +end + +BinaryOperation.MOD = function(a, b) + return a % b +end + +BinaryOperation.POW = function(a, b) + return a ^ b +end + +BinaryOperation.DEFAULT_NAMES = { + [BinaryOperation.ADD] = "+", + [BinaryOperation.SUB] = "-", + [BinaryOperation.MUL] = "*", + [BinaryOperation.DIV] = "/", + [BinaryOperation.IDIV] = "//", + [BinaryOperation.MOD] = "%", + [BinaryOperation.POW] = "^" +} + +BinaryOperation.COMMUTATIVITY = { + [BinaryOperation.ADD] = true, + [BinaryOperation.SUB] = false, + [BinaryOperation.MUL] = true, + [BinaryOperation.DIV] = false, + [BinaryOperation.IDIV] = false, + [BinaryOperation.MOD] = false, + [BinaryOperation.POW] = false +} + +BinaryOperation.ADDEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.ADD, expressions, name) +end + +BinaryOperation.SUBEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.SUB, expressions, name) +end + +BinaryOperation.MULEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.MUL, expressions, name) +end + +BinaryOperation.DIVEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.DIV, expressions, name) +end + +BinaryOperation.IDIVEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.IDIV, expressions, name) +end + +BinaryOperation.MODEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.MOD, expressions, name) +end + +BinaryOperation.POWEXP = function(expressions, name) + return BinaryOperation(BinaryOperation.POW, expressions, name) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua new file mode 100644 index 0000000000..2cc56b6600 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua @@ -0,0 +1,14 @@ +-- Seperates the various binary operations into their own files for readability + +--- Automatic simplification of difference expressions. +--- @return BinaryOperation +function BinaryOperation:simplifydifference() + local term1 = self.expressions[1] + local term2 = self.expressions[2] + + if not term2 then + return BinaryOperation(BinaryOperation.MUL, {Integer(-1), term1}):autosimplify() + end + + return BinaryOperation(BinaryOperation.ADD, {term1, BinaryOperation(BinaryOperation.MUL, {Integer(-1), term2}):autosimplify()}):autosimplify() +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua new file mode 100644 index 0000000000..35df72c440 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua @@ -0,0 +1,169 @@ +-- Seperates the various binary operations into their own files for readability + +--- Automatic simplification of power expressions. +--- @return BinaryOperation +function BinaryOperation:simplifypower() + local base = self.expressions[1] + local exponent = self.expressions[2] + + if base:isconstant() and exponent:isconstant() and exponent:getring() ~= Rational:getring() then + return self:evaluate() + end + + -- Simplifies i^x for x integer. + if base == I and exponent:isconstant() and exponent:getring() == Integer:getring() then + if exponent % Integer(4) == Integer(0) then + return Integer(1) + end + if exponent % Integer(4) == Integer(1) then + return I + end + if exponent % Integer(4) == Integer(2) then + return Integer(-1) + end + if exponent % Integer(4) == Integer(3) then + return -I + end + end + + -- Simplifies complex numbers raised to negative integer powers + if not base:isrealconstant() and base:iscomplexconstant() and exponent:isconstant() and exponent:getring() == Integer:getring() and exponent < Integer.zero() then + local a + local b + if base.operation == BinaryOperation.MUL then + a = Integer.zero() + b = base.expressions[1] + elseif base.operation == BinaryOperation.ADD and base.expressions[2] == I then + a = base.expressions[1] + b = Integer.one() + else + a = base.expressions[1] + b = base.expressions[2].expressions[1] + end + return (((a-b*I)/(a^Integer(2)+b^Integer(2)))^(-exponent)):expand():autosimplify() + end + + -- Uses the property that 0^x = 0 if x does not equal 0 + if base:isconstant() and base == base:zero() then + return Integer.zero() + end + + -- Uses the property that 1^x = 1 + if base:isconstant() and base == base:one() then + return base:one() + end + + -- Uses the property that x^0 = 1 + if exponent:isconstant() and exponent == exponent:zero() then + return exponent:one() + end + + -- Uses the property that x^1 = x + if exponent:isconstant() and exponent == exponent:one() then + return base + end + + -- Uses the property that b ^ (log(b, x)) == x + if exponent:type() == Logarithm and exponent.base == base then + return exponent.expression + end + + -- Uses the property that b ^ (a * log(b, x)) == x ^ a + if exponent.operation == BinaryOperation.MUL then + local x + local rest = Integer.one() + for _, expression in ipairs(exponent.expressions) do + if expression:type() == Logarithm and expression.base == base and not log then + x = expression.expression + else + rest = rest * expression + end + end + if x then + return (x ^ rest):autosimplify() + end + end + + -- Uses the property that (x^a)^b = x^(a*b) + if not base:isatomic() and base.operation == BinaryOperation.POW and exponent:isconstant() then + base, exponent = base.expressions[1], BinaryOperation(BinaryOperation.MUL, {exponent, base.expressions[2]}):autosimplify() + return BinaryOperation(BinaryOperation.POW, {base, exponent}):autosimplify() + end + + -- Uses the property that (x_1*x_2*...*x_n)^a = x_1^a*x_2^a*..x_n^a if a is an integer + if base.operation == BinaryOperation.MUL and exponent:type() == Integer then + local results = {} + for index, expression in ipairs(base.expressions) do + results[index] = BinaryOperation(BinaryOperation.POW, {expression, exponent}):autosimplify() + end + return BinaryOperation(BinaryOperation.MUL, results):autosimplify() + end + + -- Uses the property that sqrt(x,r)^d == sqrt(x,r/d) + if base:type() == SqrtExpression and exponent:type() == Integer and exponent > Integer.zero() then + local root = base.root + local expr = base.expression + local comm = Integer.gcd(root,exponent) + root = root / comm + local expo = exponent / comm + expr = expr ^ expo + return SqrtExpression(expr,root):autosimplify() + end + + -- Rationalizing SqrtExpressions + if base:type() == SqrtExpression and exponent:type() == Integer and base.expression:type() == Integer and exponent < Integer.zero() then + local root = base.root + local expr = base.expression + local result = (SqrtExpression(expr ^ (root - Integer.one()),root) / expr) ^ exponent:neg() + return result:autosimplify() + end + + if base:isconstant() and exponent:isconstant() and exponent:getring() == Rational.getring() then + return self --:simplifyrationalpower() + end + + -- Our expression cannot be simplified + return self +end + +-- Automatic simplification of rational power expressions +function BinaryOperation:simplifyrationalpower() + local base = self.expressions[1] + local exponent = self.expressions[2] + + if base:getring() == Rational.getring() then + return (BinaryOperation(BinaryOperation.POW, {base.numerator, exponent}):simplifyrationalpower()) / + (BinaryOperation(BinaryOperation.POW, {base.denominator, exponent}):simplifyrationalpower()) + end + + if base == Integer(-1) then + if exponent == Integer(1) / Integer(2) then + return I + end + + return self + end + + local primes = base:primefactorization() + + if primes.expressions[1] and not primes.expressions[2] then + local primeexponent = primes.expressions[1].expressions[2] + local primebase = primes.expressions[1].expressions[1] + local newexponent = primeexponent * exponent + local integerpart + if newexponent.getring() == Rational.getring() then + integerpart = newexponent.numerator // newexponent.denominator + else + integerpart = newexponent + end + + if integerpart == Integer.zero() then + return BinaryOperation(BinaryOperation.POW, {primebase, newexponent}) + end + return BinaryOperation(BinaryOperation.MUL, + {BinaryOperation(BinaryOperation.POW, {primebase, integerpart}), + BinaryOperation(BinaryOperation.POW, {primebase, newexponent - integerpart})}):autosimplify() + end + + return BinaryOperation(BinaryOperation.POW, {primes:autosimplify(), exponent}) +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua new file mode 100644 index 0000000000..b104cb5e8a --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua @@ -0,0 +1,231 @@ +-- Seperates the various binary operations into their own files for readability + +--- Automatic simplification of multiplication expressions. +--- @return BinaryOperation +function BinaryOperation:simplifyproduct() + if not self.expressions[1] then + error("Execution error: attempted to simplify empty product") + end + + if not self.expressions[2] then + return self.expressions[1] + end + + -- Uses the property that x*0=0 + for _, expression in ipairs(self.expressions) do + if expression:isconstant() and expression == expression:zero() then + return expression:zero() + end + end + + local result = self:simplifyproductrec() + + -- We don't really know what ring we are working in here, so just assume the integer ring + if not result.expressions[1] then + return Integer.one() + end + + if not result.expressions[2] then + return result.expressions[1] + end + + return result +end + +function BinaryOperation:simplifyproductrec() + local term1 = self.expressions[1] + local term2 = self.expressions[2] + + if not self.expressions[3] then + if (term1:isconstant() or not (term1.operation == BinaryOperation.MUL)) and + (term2:isconstant() or not (term2.operation == BinaryOperation.MUL)) then + + if term1:isconstant() and term2:isconstant() then + local result = self:evaluate() + if result == result:one() then + return BinaryOperation(BinaryOperation.MUL, {}) + end + return BinaryOperation(BinaryOperation.MUL, {result}) + end + + -- Uses the property that x*1 = x + if term1:isconstant() and term1 == term1:one() then + return BinaryOperation(BinaryOperation.MUL, {term2}) + end + + if term2:isconstant() and term2 == term2:one() then + return BinaryOperation(BinaryOperation.MUL, {term1}) + end + + -- Distributes constants if the other term is a sum expression. + if term1:isconstant() and term2.operation == BinaryOperation.ADD then + local distributed = BinaryOperation(BinaryOperation.ADD, {}) + for i, exp in ipairs(term2:subexpressions()) do + distributed.expressions[i] = term1 * exp + end + return BinaryOperation(BinaryOperation.MUL, {distributed:autosimplify()}) + end + + if term2:isconstant() and term1.operation == BinaryOperation.ADD then + local distributed = BinaryOperation(BinaryOperation.ADD, {}) + for i, exp in ipairs(term1:subexpressions()) do + distributed.expressions[i] = term2 * exp + end + return BinaryOperation(BinaryOperation.MUL, {distributed:autosimplify()}) + end + + -- Uses the property that sqrt(a,r)*sqrt(b,r) = sqrt(a*b,r) if a,r are positive integers + if term1:type() == SqrtExpression and term2:type() == SqrtExpression and term1.expression:isconstant() and term2.expression:isconstant() and term1.root:type() == Integer then + if term1.root == term2.root and term1.expression > Integer.zero() then + local expression = term1.expression*term2.expression + local result = SqrtExpression(expression,term1.root):autosimplify() + if result == Integer.one() then + return BinaryOperation(BinaryOperation.MUL,{}) + else + return BinaryOperation(BinaryOperation.MUL,{result}) + end + end + end + + --if term1.operation == BinaryOperation.POW and term2.operation == BinaryOperation.POW and term1.expressions[1]:type() == SqrtExpression and term2.expressions[1]:type() == SqrtExpression and term1.expressions[2]:type() == Integer and term2.expressions[2]:type() == Integer and term1.expressions[2] < Integer.zero() and term2.expressions[2] < Integer.zero() and term1.expressions[1].root == term2.expressions[1].root then + -- local expo1 = term1.expressions[2]:neg() + -- local expo2 = term2.expressions[2]:neg() + -- local root = term1.expressions[1].root + -- local expr1 = term1.expressions[1].expression + -- local expr2 = term2.expressions[1].expression + -- local result1 = BinaryOperation(BinaryOperation.POW,{SqrtExpression(expr1,root),expo1}):simplifypower() + -- local result2 = BinaryOperation(BinaryOperation.POW,{SqrtExpression(expr2,root),expo2}):simplifypower() + -- local result = BinaryOperation(BinaryOperation.MUL,{result1,result2}):autosimplify() + -- if result == Integer.one() then + -- return BinaryOperation(BinaryOperation.MUL,{}) + -- end + -- if result:type() == Integer then + -- return BinaryOperation(BinaryOperation.MUL,{Rational(Integer.one(),result)}) + -- end + -- return BinaryOperation(BinaryOperation.MUL,{BinaryOperation(BinaryOperation.POW, {result,Integer(-1)})}):autosimplify() + --end + + -- Uses the property that x^a*x^b=x^(a+b) + local revertterm1 = false + local revertterm2 = false + if term1.operation ~= BinaryOperation.POW then + term1 = BinaryOperation(BinaryOperation.POW, {term1, Integer.one()}) + revertterm1 = true + end + if term2.operation ~= BinaryOperation.POW then + term2 = BinaryOperation(BinaryOperation.POW, {term2, Integer.one()}) + revertterm2 = true + end + if term1.expressions[1] == term2.expressions[1] + --and not + -- (term1.expressions[1]:type() == Integer and + -- term1.expressions[2]:type() ~= term2.--expressions[2]:type()) + then + local result = BinaryOperation(BinaryOperation.POW, + {term1.expressions[1], + BinaryOperation(BinaryOperation.ADD, + {term1.expressions[2], term2.expressions[2]}):autosimplify()}):autosimplify() + if result:isconstant() and result == result:one() then + return BinaryOperation(BinaryOperation.MUL, {}) + end + return BinaryOperation(BinaryOperation.MUL, {result}) + end + + if revertterm1 then + term1 = term1.expressions[1] + end + if revertterm2 then + term2 = term2.expressions[1] + end + + if term2:order(term1) then + return BinaryOperation(BinaryOperation.MUL, {term2, term1}) + end + + return self + end + + if term1.operation == BinaryOperation.MUL and not (term2.operation == BinaryOperation.MUL) then + return term1:mergeproducts(BinaryOperation(BinaryOperation.MUL, {term2})) + end + + if not (term1.operation == BinaryOperation.MUL) and term2.operation == BinaryOperation.MUL then + return BinaryOperation(BinaryOperation.MUL, {term1}):mergeproducts(term2) + end + + return term1:mergeproducts(term2) + end + + local rest = {} + for index, expression in ipairs(self.expressions) do + if index > 1 then + rest[index - 1] = expression + end + end + + local result = BinaryOperation(BinaryOperation.MUL, rest):simplifyproductrec() + + if term1.operation ~= BinaryOperation.MUL then + term1 = BinaryOperation(BinaryOperation.MUL, {term1}) + end + if result.operation ~= BinaryOperation.MUL then + result = BinaryOperation(BinaryOperation.MUL, {result}) + end + return term1:mergeproducts(result) +end + +-- Merges two lists of products +function BinaryOperation:mergeproducts(other) + if not self.expressions[1] then + return other + end + + if not other.expressions[1] then + return self + end + + local first = BinaryOperation(BinaryOperation.MUL, {self.expressions[1], other.expressions[1]}):simplifyproductrec() + + local selfrest = {} + for index, expression in ipairs(self.expressions) do + if index > 1 then + selfrest[index - 1] = expression + end + end + + local otherrest = {} + for index, expression in ipairs(other.expressions) do + if index > 1 then + otherrest[index - 1] = expression + end + end + + if first.operation ~= BinaryOperation.MUL or not first.expressions[2] then + local result = BinaryOperation(self.operation, selfrest):mergeproducts(BinaryOperation(other.operation, otherrest)) + if not first.expressions[1] then + return result + end + table.insert(result.expressions, 1, first.expressions[1]) + + if result.operation == BinaryOperation.MUL and not result.expressions[3] and result.expressions[1] and result.expressions[2] then + if result.expressions[1]:isconstant() and result.expressions[2]:isconstant() then + return result:simplifyproductrec() + end + end + return result + end + + local result + if first.expressions[1] == self.expressions[1] then + result = BinaryOperation(self.operation, selfrest):mergeproducts(other) + else + result = self:mergeproducts(BinaryOperation(other.operation, otherrest)) + end + table.insert(result.expressions, 1, first.expressions[1]) + if result.operation == BinaryOperation.MUL and not result.expressions[3] and result.expressions[1] and result.expressions[2] then + if result.expressions[1]:isconstant() and result.expressions[2]:isconstant() then + return result:simplifyproductrec() + end + end + return result +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua new file mode 100644 index 0000000000..4dac489bf5 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua @@ -0,0 +1,14 @@ +-- Seperates the various binary operations into their own files for readability + +-- Automatic simplification of quotient expressions. +--- @return BinaryOperation +function BinaryOperation:simplifyquotient() + local numerator = self.expressions[1] + local denominator = self.expressions[2] + + if numerator:isconstant() and denominator:isconstant() then + return self:evaluate() + end + + return BinaryOperation(BinaryOperation.MUL, {numerator, BinaryOperation(BinaryOperation.POW, {denominator, Integer(-1)}):autosimplify()}):autosimplify() +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua new file mode 100644 index 0000000000..5bb204066f --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua @@ -0,0 +1,226 @@ +-- Seperates the various binary operations into their own files for readability + +-- Automatic simplification of addition expressions. +--- @return BinaryOperation +function BinaryOperation:simplifysum() + if not self.expressions[1] then + error("Execution error: Attempted to simplify empty sum") + end + + if not self.expressions[2] then + return self.expressions[1] + end + + local result = self:simplifysumrec() + + -- We don't really know what ring we are working in here, so just assume the integer ring + if not result.expressions[1] then + return Integer.zero() + end + + -- Simplifies single sums to their operands + if not result.expressions[2] then + return result.expressions[1] + end + + return result +end + +function BinaryOperation:simplifysumrec() + local term1 = self.expressions[1] + local term2 = self.expressions[2] + + if self.expressions[1] and self.expressions[2] and not self.expressions[3] then + if (term1:isconstant() or not (term1.operation == BinaryOperation.ADD)) and + (term2:isconstant() or not (term2.operation == BinaryOperation.ADD)) then + + if term1:isconstant() and term2:isconstant() then + return BinaryOperation(BinaryOperation.ADD, {self:evaluate()}) + end + + -- Uses the property that x + 0 = x + if term1:isconstant() and term1 == term1:zero() then + return BinaryOperation(BinaryOperation.ADD, {term2}) + end + + if term2:isconstant() and term2 == term2:zero() then + return BinaryOperation(BinaryOperation.ADD, {term1}) + end + + local revertterm1 = false + local revertterm2 = false + -- Uses the property that a*x+b*x= (a+b)*x + -- This is only done if a and b are constant, since otherwise this could be counterproductive + -- We SHOULD be okay to only check left distributivity, since constants always come first when ordered + if term1.operation == BinaryOperation.MUL and term2.operation == BinaryOperation.MUL then + local findex = 2 + local sindex = 2 + if not term1.expressions[1]:isconstant() then + revertterm1 = true + findex = 1 + end + if not term2.expressions[1]:isconstant() then + revertterm2 = true + sindex = 1 + end + if FancyArrayEqual(term1.expressions,term2.expressions,findex,sindex) then + local result + if not revertterm1 and not revertterm2 then + result = BinaryOperation( + BinaryOperation.ADD, + {term1.expressions[1],term2.expressions[1]} + ) + end + if revertterm1 and not revertterm2 then + result = BinaryOperation( + BinaryOperation.ADD, + {Integer.one(),term2.expressions[1]} + ) + end + if not revertterm1 and revertterm2 then + result = BinaryOperation( + BinaryOperation.ADD, + {term1.expressions[1],Integer.one()} + ) + end + if revertterm1 and revertterm2 then + result = Integer(2) + end + result = result:autosimplify() + for i=findex,#term1.expressions do + result = BinaryOperation( + BinaryOperation.MUL, + {result,term1.expressions[i]} + ) + end + result = result:autosimplify() + if result:isconstant() and result == result:zero() then + return BinaryOperation(BinaryOperation.ADD, {}) + end + return BinaryOperation(BinaryOperation.ADD, {result}) + end + end + + if term1.operation ~= BinaryOperation.MUL or not term1.expressions[1]:isconstant() then + term1 = BinaryOperation(BinaryOperation.MUL,{Integer.one(), term1}) + revertterm1 = true + end + if term2.operation ~= BinaryOperation.MUL or not term2.expressions[1]:isconstant() then + term2 = BinaryOperation(BinaryOperation.MUL, {Integer.one(), term2}) + revertterm2 = true + end + if ArrayEqual(term1.expressions, term2.expressions, 2) then + local result = BinaryOperation( + BinaryOperation.ADD, + {term1.expressions[1],term2.expressions[1]} + ) + result = result:autosimplify() + for i=2,#term1.expressions do + result = BinaryOperation( + BinaryOperation.MUL, + {result,term1.expressions[i]} + ) + end + result = result:autosimplify() + --local result = BinaryOperation(BinaryOperation.MUL, + -- {BinaryOperation(BinaryOperation.ADD, + -- {term1.expressions[1], + -- term2.expressions[1]}):autosimplify(), + -- term1.expressions[2]}):autosimplify() + if result:isconstant() and result == result:zero() then + return BinaryOperation(BinaryOperation.ADD, {}) + end + return BinaryOperation(BinaryOperation.ADD, {result}) + end + + if revertterm1 then + term1 = term1.expressions[2] + end + if revertterm2 then + term2 = term2.expressions[2] + end + + if term2:order(term1) then + return BinaryOperation(BinaryOperation.ADD, {term2, term1}) + end + + return self + end + + if term1.operation == BinaryOperation.ADD and not (term2.operation == BinaryOperation.ADD) then + return term1:mergesums(BinaryOperation(BinaryOperation.ADD, {term2})) + end + + if not (term1.operation == BinaryOperation.ADD) and term2.operation == BinaryOperation.ADD then + return BinaryOperation(BinaryOperation.ADD, {term1}):mergesums(term2) + end + + return term1:mergesums(term2) + end + + local rest = {} + for index, expression in ipairs(self.expressions) do + if index > 1 then + rest[index - 1] = expression + end + end + + local result = BinaryOperation(BinaryOperation.ADD, rest):simplifysumrec() + + if term1.operation ~= BinaryOperation.ADD then + term1 = BinaryOperation(BinaryOperation.ADD, {term1}) + end + if result.operation ~= BinaryOperation.ADD then + result = BinaryOperation(BinaryOperation.ADD, {result}) + end + return term1:mergesums(result) +end + +-- Merges two lists of sums +function BinaryOperation:mergesums(other) + if not self.expressions[1] then + return other + end + + if not other.expressions[1] then + return self + end + + local first = BinaryOperation(BinaryOperation.ADD, {self.expressions[1], other.expressions[1]}):simplifysumrec() + + local selfrest = {} + for index, expression in ipairs(self.expressions) do + if index > 1 then + selfrest[index - 1] = expression + end + end + + local otherrest = {} + for index, expression in ipairs(other.expressions) do + if index > 1 then + otherrest[index - 1] = expression + end + end + + if first.operation ~= BinaryOperation.ADD or not first.expressions[2] then + local result = BinaryOperation(self.operation, selfrest):mergesums(BinaryOperation(other.operation, otherrest)) + if not first.expressions[1] then + return result + end + if first.expressions[1] ~= Integer.zero(0) then + table.insert(result.expressions, 1, first.expressions[1]) + end + return result + end + + local result + if first.expressions[1] == self.expressions[1] then + result = BinaryOperation(self.operation, selfrest):mergesums(other) + else + result = self:mergesums(BinaryOperation(other.operation, otherrest)) + end + + table.insert(result.expressions, 1, first.expressions[1]) + + return result +end diff --git a/macros/luatex/latex/luacas/tex/core/compoundexpression.lua b/macros/luatex/latex/luacas/tex/core/compoundexpression.lua new file mode 100644 index 0000000000..792c36cfc2 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/compoundexpression.lua @@ -0,0 +1,52 @@ +--- @class CompoundExpression +--- Interface for an expression consisting of one or more subexpressions. +CompoundExpression = {} +__CompoundExpression = {} + +---------------------- +-- Instance methods -- +---------------------- + +--- @param symbol SymbolExpression +--- @return boolean +function CompoundExpression:freeof(symbol) + for _, expression in ipairs(self:subexpressions()) do + if not expression:freeof(symbol) then + return false + end + end + return true + end + +--- @param map table<Expression, Expression> +--- @return Expression +function CompoundExpression:substitute(map) + for expression, replacement in pairs(map) do + if self == expression then + return replacement + end + end + + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:substitute(map) + end + return self:setsubexpressions(results) +end + +--- @return boolean +function CompoundExpression:isatomic() + return false +end + +--- @return boolean +function CompoundExpression:isconstant() + return false +end + +----------------- +-- Inheritance -- +----------------- + +__CompoundExpression.__index = Expression +CompoundExpression = setmetatable(CompoundExpression, __CompoundExpression)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/constantexpression.lua b/macros/luatex/latex/luacas/tex/core/constantexpression.lua new file mode 100644 index 0000000000..0f91a037a9 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/constantexpression.lua @@ -0,0 +1,62 @@ +--- @class ConstantExpression +--- @alias Constant ConstantExpression +--- Interface for a mathematical expression without any symbols. +--- ConstantExpressions are AtomicExpressions by default, but individual classes may overwrite that inheritance. +ConstantExpression = {} +__ConstantExpression = {} + +---------------------- +-- Instance methods -- +---------------------- + + +--- @param symbol SymbolExpression +--- @return boolean +function ConstantExpression:freeof(symbol) + return true +end + +--- @return boolean +function ConstantExpression:isconstant() + return true +end + +--- @param other Expression +--- @return boolean +function ConstantExpression:order(other) + + -- Constants come before non-constants. + if not other:isconstant() then + return true + end + + if self ~= E and self ~= PI and self ~= I then + if other ~= E and other ~= PI and other ~= I then + -- If both self and other are ring elements, we use the total order on the ring to sort. + return self < other + end + -- Special constants come after ring elements. + return true + end + + -- Special constants come after ring elements. + if other ~= E and other ~= PI and other ~= I then + return false + end + + -- Ensures E < PI < I. + + if self == E then return true end + + if self == I then return false end + + return other == I +end + +----------------- +-- Inheritance -- +----------------- + +__ConstantExpression.__index = AtomicExpression +ConstantExpression = setmetatable(ConstantExpression, __ConstantExpression) + diff --git a/macros/luatex/latex/luacas/tex/core/expression.lua b/macros/luatex/latex/luacas/tex/core/expression.lua new file mode 100644 index 0000000000..8ff4659f79 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/expression.lua @@ -0,0 +1,280 @@ +--- @class Expression +--- Interface for an arbitrary mathematical expression. +Expression = {} + +---------------------- +-- Required methods -- +---------------------- + +--- Evaluates the current expression recursively by evaluating each sub-expression. +--- @return Expression +function Expression:evaluate() + error("Called unimplemented method : evaluate()") +end + +--- Performs automatic simplification of an expression. Called on every expression before being output from the CAS. +--- @return Expression +function Expression:autosimplify() + error("Called unimplemented method : autosimplify()") +end + +--- Performs more rigorous simplification of an expression. Checks different equivalent forms and determines the 'smallest' expresion. +--- @return Expression +function Expression:simplify() + local me = self:unlock():autosimplify() + local results = {} + for index, expression in ipairs(me:subexpressions()) do + results[index] = expression:simplify() + end + me = me:setsubexpressions(results) + + local out = me + local minsize = self:size() + + local test = me:expand() + if test:size() < minsize then + out = test + minsize = test:size() + end + + test = me:factor() + if test:size() < minsize then + out = test + minsize = test:size() + end + + return out +end + +--- Changes the autosimplify behavior of an expression depending on its parameters. +--- THIS METHOD MUTATES THE OBJECT IT ACTS ON. +--- @param mode number +--- @param permanent boolean +--- @param recursive boolean +--- @return Expression +function Expression:lock(mode, permanent, recursive) + function self:autosimplify() + if not permanent then + self.autosimplify = nil + end + + if mode == Expression.NIL then + return self + elseif mode == Expression.SUBS then + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:autosimplify() + end + self = self:setsubexpressions(results, true) -- TODO: Add mutate setsubexpressions + return self + end + end + if recursive then + for _, expression in ipairs(self:subexpressions()) do + expression:lock(mode, permanent, recursive) + end + end + return self +end + +--- Frees any locks on expressions. +--- THIS METHOD MUTATES THE OBJECT IT ACTS ON. +--- @param recursive boolean +--- @return Expression +function Expression:unlock(recursive) + self.autosimplify = nil + if recursive then + for _, expression in ipairs(self:subexpressions()) do + expression:unlock(recursive) + end + end + return self +end + +--- Returns a list of all subexpressions of an expression. +--- @return table<number, Expression> +function Expression:subexpressions() + error("Called unimplemented method : subexpressions()") +end + +--- Returns the total number of atomic and compound expressions that make up an expression, or the number of nodes in the expression tree. +--- @return Integer +function Expression:size() + local out = Integer.one() + for _, expression in ipairs(self:subexpressions()) do + out = out + expression:size() + end + return out +end + +--- Returns a copy of the original expression with each subexpression substituted with a new one, or a mutated version if mutate is true. +--- @param subexpressions table<number, Expression> +--- @param mutate boolean +--- @return Expression +function Expression:setsubexpressions(subexpressions, mutate) + error("Called unimplemented method : setsubexpressions()") +end + +--- Determines whether or not an expression contains a particular symbol. +--- @param symbol SymbolExpression +--- @return boolean +function Expression:freeof(symbol) + error("Called unimplemented method : freeof()") +end + +--- Substitutes occurances of specified sub-expressions with other sub-expressions. +--- @param map table<Expression, Expression> +--- @return Expression +function Expression:substitute(map) + error("Called unimplemented method : substitute()") +end + +--- Algebraically expands an expression by turning products of sums into sums of products and expanding powers. +--- @return Expression +function Expression:expand() + return self +end + +--- Attempts to factor an expression by turning sums of products into products of sums, and identical terms multiplied together into factors. +--- @return Expression +function Expression:factor() + return self +end + +--- Attempts to combine an expression by collapsing sums of expressions together into a single factor, e.g. common denominator +--- @return Expression +function Expression:combine() + return self +end + +--- Attempts to collect all occurances of an expression in this expression. +--- @param collect Expression +--- @return Expression +function Expression:collect(collect) + return self +end + +--- Returns all non-constant subexpressions of this expression - helper method for factor. +--- @return table<number, Expression> +function Expression:getsubexpressionsrec() + local result = {} + + for _, expression in ipairs(self:subexpressions()) do + if not expression:isconstant() then + result[#result+1] = expression + end + result = JoinArrays(result, expression:getsubexpressionsrec()) + end + + return result +end + +--- Determines whether an expression is atomic. +--- Atomic expressions are not necessarily constant, since polynomial rings, for instance, are atomic parts that contain symbols. +--- @return boolean +function Expression:isatomic() + error("Called unimplemented method: isatomic()") +end + +--- Determines whether an expression is a constant, i.e., an atomic expression that is not a varaible and cannot be converted into an equivalent compound expression. +--- @return boolean +function Expression:isconstant() + error("Called unimplemented method: isconstant()") +end + +--- Determines whether an expression is a 'proper' real constant, i.e., is free of every varaible. +function Expression:isrealconstant() + if self:isconstant() or self == PI or self == E then + return true + end + + for _, expression in ipairs(self:subexpressions()) do + if not expression:isrealconstant() then + return false + end + end + + return self:type() ~= SymbolExpression +end + +--- Determines whether an expression is a 'proper' complex constant, i.e., is free of every varaible and is of the form a + bI for nonzero a and b. +--- @return boolean +function Expression:iscomplexconstant() + return self:isrealconstant() or (self.operation == BinaryOperation.ADD and #self.expressions == 2 and self.expressions[1]:isrealconstant() + and ((self.expressions[2].operation == BinaryOperation.MUL and #self.expressions[2].expressions == 2 and self.expressions[2].expressions[1]:isrealconstant() and self.expressions[2].expressions[2] == I) + or self.expressions[2] == I)) or (self.operation == BinaryOperation.MUL and #self.expressions == 2 and self.expressions[1]:isrealconstant() and self.expressions[2] == I) +end + +--- A total order on autosimplified expressions. Returns true if self < other. +--- @param other Expression +--- @return boolean +function Expression:order(other) + error("Called unimplemented method: order()") +end + +--- Returns an autosimplified expression as a single-variable polynomial in a ring, if it can be converted. Returns itself otherwise. +--- @return PolynomialRing, boolean +function Expression:topolynomial() + return self, false +end + +--- Converts this expression to LaTeX code. +--- @return string +function Expression:tolatex() + error("Called Unimplemented method: tolatex()") +end + +---------------------- +-- Instance methods -- +---------------------- + +--- Returns the type of the expression, i.e., the table used to create objects of that type. +--- @return table +function Expression:type() + return getmetatable(self).__index +end + +-------------------------- +-- Instance metamethods -- +-------------------------- + +__ExpressionOperations = {} + +__ExpressionOperations.__unm = function(a) + return BinaryOperation.SUBEXP({a}) +end + +__ExpressionOperations.__add = function(a, b) + return BinaryOperation.ADDEXP({a, b}) +end + +__ExpressionOperations.__sub = function(a, b) + return BinaryOperation.SUBEXP({a, b}) +end + +__ExpressionOperations.__mul = function(a, b) + return BinaryOperation.MULEXP({a, b}) +end + +__ExpressionOperations.__div = function(a, b) + return BinaryOperation.DIVEXP({a, b}) +end + +__ExpressionOperations.__pow = function(a, b) + return BinaryOperation.POWEXP({a, b}) +end + +-- For iterating over the subexpressions of a easily. +__ExpressionOperations.__call = function(a, ...) + if a:type() == SymbolExpression then + return FunctionExpression(a, table.pack(...)) + end + return BinaryOperation.MULEXP({a, table.pack(...)[1]}) +end + +---------------------- +-- Static constants -- +---------------------- + +Expression.NIL = 0 +Expression.SUBS = 1
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/functionexpression.lua b/macros/luatex/latex/luacas/tex/core/functionexpression.lua new file mode 100644 index 0000000000..dc94790b81 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/functionexpression.lua @@ -0,0 +1,295 @@ +--- @class FunctionExpression +--- Represents a generic function that takes zero or more expressions as inputs. +--- @field name SymbolExpression +--- @field expressions table<number, Expression> +--- @field orders table<number, Integer> +--- @field variables table<number,SymbolExpression> +--- @alias Function FunctionExpression +FunctionExpression = {} +__FunctionExpression = {} + +---------------------------- +-- Instance functionality -- +---------------------------- + +--- Creates a new function expression with the given operation. +--- @param name string|SymbolExpression +--- @param expressions table<number, Expression> +--- @param derivatives table<number,Integer> +--- @return FunctionExpression +function FunctionExpression:new(name, expressions, derivatives) + local o = {} + local __o = Copy(__ExpressionOperations) + + if type(name) == "table" and name:type() == SymbolExpression then + name = name.symbol + end + + if TrigExpression.NAMES[name] and #expressions == 1 then + return TrigExpression(name, expressions[1]) + end + + -- TODO: Symbol Checking For Constructing derivatives like this + --if string.sub(name, #name, #name) == "'" and #expressions == 1 then + -- return DerivativeExpression(FunctionExpression(string.sub(name, 1, #name - 1), expressions), SymbolExpression("x"), true) + --end + + o.name = name + o.expressions = Copy(expressions) + o.variables = Copy(expressions) + for _,expression in ipairs(o.variables) do + if not expression:isatomic() then + o.variables = {} + if #o.expressions < 4 then + local defaultvars = {SymbolExpression('x'),SymbolExpression('y'),SymbolExpression('z')} + for i=1,#o.expressions do + o.variables[i] = defaultvars[i] + end + else + for i=1,#o.expressions do + o.variables[i] = SymbolExpression('x_'..tostring(i)) + end + end + end + end + if derivatives then + o.derivatives = Copy(derivatives) + else + o.derivatives = {} + for i=1,#o.variables do + o.derivatives[i] = Integer.zero() + end + end + + __o.__index = FunctionExpression + __o.__tostring = function(a) + local total = Integer.zero() + for _,integer in ipairs(a.derivatives) do + total = total + integer + end + if total == Integer.zero() then + local out = a.name .. '(' + for index, expression in ipairs(a.expressions) do + out = out .. tostring(expression) + if a.expressions[index + 1] then + out = out .. ', ' + end + end + return out .. ')' + else + local out = 'd' + if total > Integer.one() then + out = out ..'^' .. tostring(total) + end + out = out .. a.name .. '/' + for index,integer in ipairs(a.derivatives) do + if integer > Integer.zero() then + out = out .. 'd' .. tostring(a.variables[index]) + if integer > Integer.one() then + out = out .. '^' .. tostring(integer) + end + end + end + out = out .. '(' + for index, expression in ipairs(a.expressions) do + out = out .. tostring(expression) + if a.expressions[index + 1] then + out = out .. ', ' + end + end + return out .. ')' + end + end + __o.__eq = function(a, b) + -- if b:type() == TrigExpression then + -- return a == b:tofunction() + -- end + if b:type() ~= FunctionExpression then + return false + end + if #a.expressions ~= #b.expressions then + return false + end + for index, _ in ipairs(a.expressions) do + if a.expressions[index] ~= b.expressions[index] then + return false + end + end + for index,_ in ipairs(a.derivatives) do + if a.derivatives[index] ~= b.derivatives[index] then + return false + end + end + return a.name == b.name + end + + o = setmetatable(o, __o) + + return o +end + +--- @return FunctionExpression +function FunctionExpression:evaluate() + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:evaluate() + end + local result = FunctionExpression(self.name, results, self.derivatives) + result.variables = self.variables + return result +end + +--- @return FunctionExpression +function FunctionExpression:autosimplify() + -- Since the function is completely generic, we can't really do anything execpt autosimplify subexpressions. + local results = {} + for index, expression in ipairs(self:subexpressions()) do + results[index] = expression:autosimplify() + end + local result = FunctionExpression(self.name, results, self.derivatives) + result.variables = self.variables + return result +end + +--- @return table<number, Expression> +function FunctionExpression:subexpressions() + return self.expressions +end + +--- @param subexpressions table<number, Expression> +--- @return FunctionExpression +function FunctionExpression:setsubexpressions(subexpressions) + local result = FunctionExpression(self.name, subexpressions, self.derivatives) + result.variables = self.variables + return result +end + +--- @param other Expression +--- @return boolean +function FunctionExpression:order(other) + if other:isatomic() then + return false + end + + -- CASC Autosimplfication has some symbols appearing before functions, but that looks bad to me, so all symbols appear before products now. + -- if other:type() == SymbolExpression then + -- return SymbolExpression(self.name):order(other) + -- end + + if other:type() == BinaryOperation then + if other.operation == BinaryOperation.ADD or other.operation == BinaryOperation.MUL then + return BinaryOperation(other.operation, {self}):order(other) + end + + if other.operation == BinaryOperation.POW then + return (self^Integer.one()):order(other) + end + end + + if other:type() == SqrtExpression then + return self:order(other:topower()) + end + + -- TODO: Make Logarithm and AbsExpression inherit from function expression to reduce code duplication + if other:type() == Logarithm then + return self:order(FunctionExpression("log", {other.base, other.expression})) + end + + if other:type() ~= FunctionExpression and other:type() ~= TrigExpression then + return true + end + + if self.name ~= other.name then + return SymbolExpression(self.name):order(SymbolExpression(other.name)) + end + + local k = 1 + while self:subexpressions()[k] and other:subexpressions()[k] do + if self:subexpressions()[k] ~= other:subexpressions()[k] then + return self:subexpressions()[k]:order(other:subexpressions()[k]) + end + k = k + 1 + end + return #self.expressions < #other.expressions +end + +--- @return string +function FunctionExpression:tolatex() + local out = tostring(self.name) + if self:type() == TrigExpression then + out = "\\" .. out + end + if self:type() ~= TrigExpression and #self.name>1 then + --if out:sub(2,2) ~= "'" then + --local fp = out:find("'") + --if fp then + -- out = '\\operatorname{' .. out:sub(1,fp-1) .. '}' .. out:sub(fp,-1) + --else + out = '\\operatorname{' .. out .. '}' + --end + --end + end + local total = Integer.zero() + for _,integer in ipairs(self.derivatives) do + total = total + integer + end + if #self.expressions == 1 then + if total == Integer.zero() then + goto continue + else + if total < Integer(5) then + while total > Integer.zero() do + out = out .. "'" + total = total - Integer.one() + end + else + out = out .. '^{(' .. total:tolatex() .. ')}' + end + end + end + if #self.expressions > 1 then + if total == Integer.zero() then + goto continue + else + if total < Integer(4) then + out = out .. '_{' + for index,integer in ipairs(self.derivatives) do + local i = integer:asnumber() + while i > 0 do + out = out .. self.variables[index]:tolatex() + i = i - 1 + end + end + out = out .. '}' + else + out = '\\frac{\\partial^{' .. total:tolatex() .. '}' .. out .. '}{' + for index, integer in ipairs(self.derivatives) do + if integer > Integer.zero() then + out = out .. '\\partial ' .. self.variables[index]:tolatex() + if integer ~= Integer.one() then + out = out .. '^{' .. integer:tolatex() .. '}' + end + end + end + out = out .. '}' + end + end + end + ::continue:: + out = out ..'\\mathopen{}' .. '\\left(' + for index, expression in ipairs(self:subexpressions()) do + out = out .. expression:tolatex() + if self:subexpressions()[index + 1] then + out = out .. ', ' + end + end + return out .. '\\right)' +end + +----------------- +-- Inheritance -- +----------------- + +__FunctionExpression.__index = CompoundExpression +__FunctionExpression.__call = FunctionExpression.new +FunctionExpression = setmetatable(FunctionExpression, __FunctionExpression)
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/core/symbolexpression.lua b/macros/luatex/latex/luacas/tex/core/symbolexpression.lua new file mode 100644 index 0000000000..0d9192c084 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/core/symbolexpression.lua @@ -0,0 +1,132 @@ +--- @class SymbolExpression +--- An atomic expression corresponding to a symbol representing an arbitary value. +--- @field symbol string +--- @alias Symbol SymbolExpression +SymbolExpression = {} +__SymbolExpression = {} + +---------------------- +-- Instance methods -- +---------------------- + +--- Given the name of the symbol as a string, creates a new symbol. +--- @param symbol string +--- @return SymbolExpression +function SymbolExpression:new(symbol) + local o = {} + local __o = Copy(__ExpressionOperations) + __o.__index = SymbolExpression + __o.__tostring = function(a) + return a.symbol + end + __o.__eq = function(a, b) + return a.symbol == b.symbol + end + + if type(symbol) ~= "string" then + error("Sent parameter of wrong type: symbol must be a string") + end + + o.symbol = symbol + o = setmetatable(o, __o) + + return o +end + +--- @return boolean +function SymbolExpression:freeof(symbol) + return symbol~=self +end + +--- @return boolean +function SymbolExpression:isconstant() + return false +end + +--- @param other Expression +--- @return boolean +function SymbolExpression:order(other) + + -- Symbol Expressions come after constant expressions. + if other:isconstant() then + return false + end + + -- Lexographic order on symbols. + if other:type() == SymbolExpression then + for i = 1, math.min(#self.symbol, #other.symbol) do + if string.byte(self.symbol, i) ~= string.byte(other.symbol, i) then + return string.byte(self.symbol, i) < string.byte(other.symbol, i) + end + end + + return #self.symbol < #other.symbol + end + + if other.operation == BinaryOperation.POW then + return BinaryOperation(BinaryOperation.POW, {self, Integer.one()}):order(other) + end + + if other.operation == BinaryOperation.MUL then + return BinaryOperation(BinaryOperation.MUL, {self}):order(other) + end + + if other.operation == BinaryOperation.ADD then + return BinaryOperation(BinaryOperation.ADD, {self}):order(other) + end + + -- CASC Autosimplfication has some symbols appearing before functions, but that looks bad to me, so all symbols appear before products now. + if other:type() == FunctionExpression or other:type() == TrigExpression or other:type() == Logarithm then + return true + end + + return false +end + +--- Converts this symbol to an element of a polynomial ring. +--- @return PolynomialRing, boolean +function SymbolExpression:topolynomial() + return PolynomialRing({Integer.zero(), Integer.one()}, self.symbol), true +end + +----------------- +-- Inheritance -- +----------------- + +__SymbolExpression.__index = AtomicExpression +__SymbolExpression.__call = SymbolExpression.new +SymbolExpression = setmetatable(SymbolExpression, __SymbolExpression) + + +---------------------- +-- Static Constants -- +---------------------- + +-- The constant pi. +PI = SymbolExpression("pi") +function PI:tolatex() + return "\\pi " +end + +-- Approximates pi as a rational number. Uses continued fraction expansion. +function PI:approximate() + return Integer(313383936) / Integer(99753205) +end + +--function PI:isconstant() +-- return true +--end + +-- The constant e. +E = SymbolExpression("e") + +-- Approximates pi as a rational number. Uses continued fraction expansion. +function E:approximate() + return Integer(517656) / Integer(190435) +end + +-- The imaginary constant i. +I = SymbolExpression("i") +function I:tolatex() + return "i" +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/luacas.sty b/macros/luatex/latex/luacas/tex/luacas.sty new file mode 100644 index 0000000000..461fae6efa --- /dev/null +++ b/macros/luatex/latex/luacas/tex/luacas.sty @@ -0,0 +1,312 @@ +% Permission is granted to copy, distribute and/or modify this +% software under the terms of the LaTeX Project Public License +% (LPPL), version 1.3c or any later version. +% +% This software is provided 'as is', without warranty of any kind, +% either expressed or implied, including, but not limited to, the +% implied warranties of merchantability and fitness for a +% particular purpose. + +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{luacas} + [2022/05/07 v1.0 CAS written in Lua for LaTeX] + +\RequirePackage{iftex} +\ifluatex + \RequirePackage{luacode} +\else + {\PackageError{luacas} + {Not running under LuaLaTeX} + {This package requires LuaLaTeX. Try compiling this document with\MessageBreak 'lualatex' instead of 'latex'. This is a fatal error; I'm aborting now.}% + }\stop +\fi + +%Required Packages +\RequirePackage{xparse} +\RequirePackage{pgfkeys} +\RequirePackage{verbatim} +\RequirePackage{tikz} +\RequirePackage{xcolor} +\RequirePackage{mathtools} + +%These files contain Lua code for parsing luacas output; they also initialize the CAS itself +\directlua{require('test.parser') + require('test.helper') +} + +\NewDocumentEnvironment{CAS}% + {+b}% + {\luaexec{CASparse([[#1]])}}% + {} + +\newcommand{\get}% + [2][true]% + {\directlua{disp(#2, #1)}} + +\newcommand{\fetch}[1]{ + \directlua{tex.print(tostring(#1))} +} + +\NewDocumentCommand{\store}{m O{#1}}{ + \expandafter\def\csname #2\endcsname{% + \directlua{ + input = #1 + if not input[1] then + tex.sprint{tostring(input)} + else + tex.sprint("{") + for _,entry in ipairs(input) do + tex.sprint(tostring(entry),",") + end + tex.sprint("}") + end + }% + }% +}% + +\NewDocumentCommand{\yoink}{m O{#1}}{% +\expandafter\def\csname #2\endcsname{% + \directlua{ + tex.print(tostring(#1)) + }% + }% +} + +%\newcommand{\eval}[1]{\luaexec{tex.print(parse('#1'):tolatex())}} + +%%%%%%%%%% +%% Core %% +%%%%%%%%%% + +%pretty print + +\NewDocumentCommand{\print}{s m}{% + \IfBooleanTF{#1}{% + \directlua{ + local sym = #2 + if sym then + tex.print(sym:autosimplify():tolatex()) + else + tex.print('nil') + end + }% + }{% + \directlua{ + local sym = #2 + if sym then + tex.print(sym:tolatex()) + else + tex.print('nil') + end + }% + }% +} + +%verbatim print + +\NewDocumentCommand{\vprint}{s m}{% + \IfBooleanTF{#1}{% + \directlua{ + local sym = #2 + tex.sprint([[\unexpanded{\begin{verbatim}]]..tostring(sym)..[[\end{verbatim}}]]) + }% + }{% + \directlua{ + local sym = #2 + tex.sprint([[\unexpanded{\begin{verbatim}]]..tostring(sym:autosimplify())..[[\end{verbatim}}]]) + }% + }% +} + +\NewDocumentCommand{\lprint}{m O{nil,nil}}{% + \luaexec{ + local tbl = #1 + local low,upp = #2 + local tmp =0 + if tbl[0] == nil then + tmp = 1 + end + upp = upp or \#tbl + low = low or tmp + for i=low,upp do + tex.print(tbl[i]:tolatex()) + if tbl[i+1] then + tex.print(",") + end + end + } +} + +%prints the first level of an expression tree; for use within a tikzpicture environment + +\NewDocumentCommand{\printshrub}{s m}{% + \IfBooleanTF{#1}{% + \directlua{ + local sym = #2 + sym = sym:autosimplify() + tex.print("\\node [label=90:",whatis(sym),"] {",nameof(sym),"}") + tex.print(sym:gettheshrub()) + tex.print(";") + }% + }{% + \directlua{ + local sym = #2 + tex.print("\\node [label=90:",whatis(sym),"] {",nameof(sym),"}") + tex.print(sym:gettheshrub()) + tex.print(";") + }% + } +} + +%prints the full expression tree; for use within a tikzpicture environment + +\NewDocumentCommand{\printtree}{s m}{% + \IfBooleanTF{#1}{% + \luaexec{ + local sym = #2 + sym = sym:autosimplify() + tex.print("\\node {",nameof(sym),"}") + tex.print(sym:getthetree()) + tex.print(";") + }% + }{% + \luaexec{ + local sym = #2 + tex.print("\\node {",nameof(sym),"}") + tex.print(sym:getthetree()) + tex.print(";") + }% + } +} + +%parses an expression tree for use within the forest environment; result is stored in \forestresult + +\NewDocumentCommand{\parseforest}{s m}{% + \IfBooleanTF{#1}{% + \luaexec{ + local sym = #2 + sym = sym:autosimplify() + tex.print("\\def\\forestresult{") + tex.print("[") + tex.print(nameof(sym)) + tex.print(sym:gettheforest()) + tex.print("]") + tex.print("}") + }% + }{% + \luaexec { + local sym = #2 + tex.print("\\def\\forestresult{") + tex.print("[") + tex.print(nameof(sym)) + tex.print(sym:gettheforest()) + tex.print("]") + tex.print("}") + }% + } +} + +\NewDocumentCommand{\parseshrub}{s m}{% + \IfBooleanTF{#1}{% + \luaexec{ + local sym = #2 + sym = sym:autosimplify() + tex.print("\\def\\shrubresult{") + tex.print("[") + tex.print(nameof(sym)) + tex.print(", tikz+={\\node[anchor=south] at (.north) {test};}") + tex.print(sym:getthefancyshrub()) + tex.print("]") + tex.print("}") + }% + }{% + \luaexec{ + local sym = #2 + tex.print("\\def\\shrubresult{") + tex.print("[") + tex.print(nameof(sym)) + tex.print(", tikz+={\\node[anchor=south,font=\\ttfamily\\footnotesize,gray] at (.north) {",longwhatis(sym),"};}") + tex.print(sym:getthefancyshrub()) + tex.print("]") + tex.print("}") + }% + } +} + +\NewDocumentCommand{\whatis}{m}{% + \luaexec{ + tex.sprint("{\\ttfamily",longwhatis(#1),"}") + }% +} + +\NewDocumentCommand{\freeof}{s m m}{% + \IfBooleanTF{#1}{% + \luaexec{ + local sym1 = #2 + local sym2 = #3 + if sym1:freeof(sym2) then + tex.print(1) + else + tex.print(0) + end + } + }{% + \luaexec{ + local sym1 = #2 + local sym2 = #3 + sym1 = sym1:autosimplify() + sym2 = sym2:autosimplify() + if sym1:freeof(sym2) then + tex.print(1) + else + tex.print(0) + end + }% + }% +} + +\NewDocumentCommand{\isatomic}{s m}{% + \IfBooleanTF{#1}{ + \luaexec{ + local sym = #2 + if sym:isatomic() then + tex.print(1) + else + tex.print(0) + end + } + }{% + \luaexec{ + local sym = #2 + sym = sym:autosimplify() + if sym:isatomic() then + tex.print(1) + else + tex.print(0) + end + }% + }% +} + +\NewDocumentCommand{\isconstant}{s m}{% + \IfBooleanTF{#1}{% + \luaexec{ + local sym = #2 + if sym:isconstant() then + tex.print(1) + else + tex.print(0) + end + }% + }{% + \luaexec{% + local sym = #2 + sym = sym:autosimplify() + if sym:isconstant() then + tex.print(1) + else + tex.print(0) + end + }% + }% +} diff --git a/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua b/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua new file mode 100644 index 0000000000..5d5429f59b --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua @@ -0,0 +1,30 @@ +local a = DD(SymbolExpression("x") * SymbolExpression("y"), SymbolExpression("x")) +local b = DD(Integer(3) * SymbolExpression("x") ^ Integer(2) + Integer(2) * SymbolExpression("x") + Integer(6), SymbolExpression("x")) +local c = DD(E ^ SymbolExpression("x"), SymbolExpression("x")) +local d = DD(FunctionExpression("f", {SymbolExpression("x") ^ Integer(2)})) +local e = DD(SymbolExpression("x") ^ SymbolExpression("x")) +local f = DD(PolynomialRing({Integer(3), Integer(4), Integer(5)}, "x")) +local g = DD(LN(SymbolExpression("y")), SymbolExpression("y")) +local h = DD(SymbolExpression("x") ^ SymbolExpression("n")) +local i = DD(SIN((SymbolExpression("x")))) +local j = DD(SIN(Integer(2) * COS(SymbolExpression("x")))) +local k = DD(ARCTAN(SymbolExpression("x") ^ (Integer(1) / Integer(2)))) +local l = DD(ARCSEC(SymbolExpression("x"))) + +starttest("derivatives") + +testeq(a, dparse("DD(x*y, x)")) +testeq(a:autosimplify(), parse("y"), a) +testeq(b:autosimplify(), parse("6 * x + 2"), b) +testeq(c:autosimplify(), parse("e^x"), c) +testeq(d:autosimplify(), (Integer(2) * SymbolExpression("x") * FunctionExpression("f", {SymbolExpression("x")^Integer(2)}, {Integer(1)})):autosimplify(), d) +testeq(e:autosimplify(), parse("x^x * (1 + ln(x))"), e) +testeq(f:autosimplify(), parse("4 + 10 * x"), f) +testeq(g:autosimplify(), parse("y ^ -1"), g) +testeq(h:autosimplify(), parse("n * x ^ (-1 + n)"), h) +testeq(i:autosimplify(), parse("cos(x)"), i) +testeq(j:autosimplify(), parse("-2 * cos(2 * cos(x)) * sin(x)"), j) +testeq(k:autosimplify(), parse("1/2 * x^(-1/2) * (1+x) ^ -1"), k) +testeq(l:autosimplify(), parse("abs(x)^-1 * (1 + -(x^2))^ (-1/2)"), l) + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua b/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua new file mode 100644 index 0000000000..0c12f7a787 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua @@ -0,0 +1,54 @@ +local a = dparse("int(x^2, x)") +local b = dparse("int(x^-1, x, 1, e)") +local c = dparse("int(3*x^2+2*x+6, x)") +local d = dparse("int(sin(x)*cos(x), x)") +local e = dparse("int(2*x*cos(x^2), x)") +local f = dparse("int(sin(2*x), x)") +local g = dparse("int(e^sin(x), x)") +local h = dparse("int((1 / (1 + (1 / x))), x)") +local i = dparse("int(e^(x^(1/2)), x)") +local j = dparse("int((x^3+1)/(x-2), x)") +local k = dparse("int((x^2-x+1)/(x^3+3*x^2+3*x+1), x)") +local l = dparse("int(1 / (x^3+6*x), x)") +local m = dparse("int(1/(x^2+x+1), x)") +local n = dparse("int(1/(x^3+2*x+2), x, 0, 1)") + +local o = dparse("int(x^2*e^x, x)") +local p = dparse("int((x^2+6*x+3)*sin(x), x)") +local q = dparse("int(x*e^x*sin(x),x)") +local r = dparse("int(cos(x)^3, x)") +local s = dparse("int(1/(e^x+1), x)") +local t = dparse("int(e^(2*x)*cos(3*x), x)") +local u = dparse("int((x^2-1)^2, x, -1, 1)") + +starttest("integration") +testeq(a, dparse("int(x ^ 2, x)")) +testeq(a:autosimplify(), parse("x^3/3"), a) +testeq(b:autosimplify(), parse("1"), b) +testeq(c:autosimplify(), parse("x^3+x^2+6*x"), c) +testeq(d:autosimplify(), parse("(-1/2 * (cos(x) ^ 2))"), d) +testeq(e:autosimplify(), parse("sin((x ^ 2))"), e) +testeq(f:autosimplify(), parse("(-1/2 * cos((2 * x)))"), f) +-- testeq(g:autosimplify(), dparse("int(e ^ (sin(x)), x)"), g) +testeq(h:autosimplify(), parse("x + (-1 * (log(e, 1 + (x ^ -1)))) + (-1 * (log(e, x)))"), h) +testeq(i:autosimplify(), parse("-2 * (e ^ (x ^ (1/2))) + 2 * (e ^ (x ^ (1/2))) * (x ^ (1/2))"), i) +testeq(j:autosimplify(), parse("((4 * x) + (x ^ 2) + (1/3 * (x ^ 3)) + (9 * log(e, (-2 + x))))"), j) +testeq(k:autosimplify(), parse("((-3/2 * ((1 + x) ^ -2)) + (3 * ((1 + x) ^ -1)) + log(e, (1 + x)))"), k) +testeq(l:autosimplify(), parse("((1/6 * log(e, x)) + (-1/12 * log(e, (6 + (x ^ 2)))))"), l) +testeq(m:autosimplify(), parse("2/3 * (3 ^ (1/2)) * (arctan((3 ^ (1/2)) * (1/3 + (2/3 * x))))"), m) +-- test(n:autosimplify(), [[((((6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) +-- * log(e, (18 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-264600 + (1/2 +-- * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((-1/840 + (1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/280 + (-1/280 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))) + (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/280 + (-1/280 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 +-- ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))) + (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * +-- ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 * +-- (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-1/2 + (1/2 * (-3 ^ 1/2))) +-- ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))))]], n) + +testeq(o:autosimplify(), parse("((2 * (e ^ x)) + (-2 * (e ^ x) * x) + ((e ^ x) * (x ^ 2)))"), o) +testeq(p:autosimplify(), parse("((2 * cos(x)) + ((-3 + (-6 * x) + (-1 * (x ^ 2))) * cos(x)) + ((6 + (2 * x)) * sin(x)))"), p) +testeq(q:autosimplify(), parse("(1/2 * (e ^ x) * (cos(x))) + (-1/2 * (e ^ x) * x * (cos(x))) + (1/2 * (e ^ x) * x * (sin(x)))"), q) +testeq(r:autosimplify(), parse("((3/4 * sin(x)) + (1/12 * sin((3 * x))))"), r) +testeq(s:autosimplify(), parse("log(e, 1 + (-1 * ((1 + (e ^ x)) ^ -1)))"), s) +testeq(t:autosimplify(), parse("(2/13 * (e ^ (2 * x)) * (cos(3 * x))) + (3/13 * (e ^ (2 * x)) * (sin(3 * x)))"), t) +testeq(u:autosimplify(), parse("16/15"), u) +endtest() diff --git a/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua b/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua new file mode 100644 index 0000000000..09ad9874cd --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua @@ -0,0 +1,199 @@ +local a = BinaryOperation.ADDEXP + ({Integer(3), + Integer(5)}) + +local b = BinaryOperation.MULEXP + ({BinaryOperation.ADDEXP + ({Integer(13), + Integer(12)}), + Integer(-4)}) + +local c = BinaryOperation.DIVEXP + ({SymbolExpression("x"), + SymbolExpression("y")}) + +local d = BinaryOperation.DIVEXP + ({BinaryOperation.ADDEXP + ({Integer(4), + Integer(-3)}), + SymbolExpression("y")}) + +local e = BinaryOperation.ADDEXP + ({Integer(3), + Integer(4), + Integer(5), + Integer(6)}) + +starttest("expression construction") +testeq(a, "3 + 5") +testeq(b, "(13 + 12) * -4") +testeq(c, "x / y") +testeq(d, "(4 + -3) / y") +testeq(e, "3 + 4 + 5 + 6") +endtest() + +starttest("expression evaluation...") +testeq(a:evaluate(), dparse("8")) +testeq(b:evaluate(), dparse("-100")) +testeq(c:evaluate(), dparse("(x / y)")) +testeq(d:evaluate(), dparse("(1 / y)")) +testeq(e:evaluate(), dparse("18")) +endtest() + +local g = BinaryOperation.POWEXP + ({Integer(0), + SymbolExpression("x")}) + +local h = BinaryOperation.POWEXP + ({Integer(1), + SymbolExpression("x")}) + +local i = BinaryOperation.POWEXP + ({SymbolExpression("x"), + Integer(0)}) + +local j = BinaryOperation.POWEXP + ({SymbolExpression("x"), + Integer(1)}) + +local k = BinaryOperation.POWEXP + ({SymbolExpression("x"), + SymbolExpression("y")}) + +local l = BinaryOperation.POWEXP + ({BinaryOperation.POWEXP + ({BinaryOperation.POWEXP + ({SymbolExpression("x"), + Integer(3)}), + Integer(4)}), + Integer(5)}) + +local m = BinaryOperation.POWEXP + ({BinaryOperation.MULEXP + ({SymbolExpression("x"), + SymbolExpression("y")}), + SymbolExpression("a")}) + + local n = BinaryOperation.MULEXP + ({SymbolExpression("x"), + SymbolExpression("y"), + Integer(0), + Integer(-2) + }) + +local o = BinaryOperation.MULEXP + ({SymbolExpression("x"), + BinaryOperation.MULEXP + ({SymbolExpression("y"), + SymbolExpression("z")})}) + +local p = BinaryOperation.MULEXP + ({SymbolExpression("x")}) + +local q = BinaryOperation.MULEXP + ({SymbolExpression("x"), SymbolExpression("x"), SymbolExpression("x"), SymbolExpression("x")}) + +local r = BinaryOperation.MULEXP + ({SymbolExpression("x"), Integer(3), SymbolExpression("a")}) + + local s = BinaryOperation.ADDEXP + ({SymbolExpression("x")}) + +local t = BinaryOperation.ADDEXP + ({SymbolExpression("x"), + BinaryOperation.ADDEXP + ({Integer(3), + SymbolExpression("y")})}) + +local u = BinaryOperation.ADDEXP + ({BinaryOperation.MULEXP + ({SymbolExpression("x"), + SymbolExpression("y")}), + BinaryOperation.MULEXP + ({SymbolExpression("y"), + SymbolExpression("x")})}) + +local v = BinaryOperation.ADDEXP + ({Integer(3), + BinaryOperation.ADDEXP + ({BinaryOperation.MULEXP + ({Integer(2), + BinaryOperation.POWEXP + ({SymbolExpression("x"), + Integer(2)})}), + BinaryOperation.ADDEXP + ({BinaryOperation.MULEXP + ({Integer(1), + SymbolExpression("y")}), + BinaryOperation.MULEXP + ({Integer(0), + SymbolExpression("x")})})}), + Integer(6)}) + + local w = BinaryOperation.MULEXP + ({BinaryOperation.DIVEXP + ({Integer(1), + SymbolExpression("x")}), + SymbolExpression("x")}) + +local x = BinaryOperation.MULEXP + ({BinaryOperation.DIVEXP + ({SymbolExpression("y"), + SymbolExpression("x")}), + BinaryOperation.DIVEXP + ({SymbolExpression("x"), + SymbolExpression("y")})}) + +local y = BinaryOperation.MULEXP + ({BinaryOperation.DIVEXP + ({Integer(1), + Integer(3)}), + SymbolExpression("x")}) + +local z = BinaryOperation.ADDEXP + ({SymbolExpression("x"), + SymbolExpression("y"), + BinaryOperation.SUBEXP + ({SymbolExpression("x"), + SymbolExpression("y")})}) + +local A = dparse("(-aa-x)+(x+aa)") + +starttest("expression autosimplification") +testeq(g:autosimplify(), parse("0"), g) +testeq(h:autosimplify(), parse("1"), h) +testeq(i:autosimplify(), parse("1"), i) +testeq(j:autosimplify(), parse("x"), j) +testeq(k:autosimplify(), parse("x ^ y"), k) +testeq(l:autosimplify(), parse("(x ^ 60)"), l) +testeq(m:autosimplify(), parse("((x * y) ^ a)"), m) +testeq(n:autosimplify(), parse("0"), n) +testeq(o:autosimplify(), parse("(x * y * z)"), o) +testeq(p:autosimplify(), parse("x"), p) +testeq(q:autosimplify(), parse("(x ^ 4)"), q) +testeq(r:autosimplify(), parse("(3 * a * x)"), r) +testeq(s:autosimplify(), parse("x"), s) +testeq(t:autosimplify(), parse("(3 + x + y)"), t) +testeq(u:autosimplify(), parse("(2 * x * y)"), u) +testeq(v:autosimplify(), parse("(9 + (2 * (x ^ 2)) + y)"), v) +testeq(w:autosimplify(), parse("1"), w) +testeq(x:autosimplify(), parse("1"), x) +testeq(y:autosimplify(), parse("(1/3 * x)"), y) +testeq(z:autosimplify(), parse("(2 * x)"), z) +testeq(A:autosimplify(), parse("0"), A) +endtest() + + +local aa = SymbolExpression("x") + SymbolExpression("y") + SymbolExpression("z") +local ab = -(SymbolExpression("x") / SymbolExpression("y")) +local ac = Integer(2)*SymbolExpression("x")*SymbolExpression("y") - Integer(3)*SymbolExpression("x")*SymbolExpression("z") + +starttest("metamethod expressions") + +testeq(aa, dparse("(x + y) + z")) +testeq(aa:autosimplify(), parse("(x + y + z)"), aa) +testeq(ab, dparse("- (x / y)")) +testeq(ab:autosimplify(), parse("(-1 * x * (y ^ -1))"), ab) +testeq(ac:autosimplify(), parse("((2 * x * y) + (-3 * x * z))"), ac) + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/collect.lua b/macros/luatex/latex/luacas/tex/test/expressions/collect.lua new file mode 100644 index 0000000000..b40e8e3d09 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/collect.lua @@ -0,0 +1,26 @@ +local x = SymbolExpression("x") +local ex = parse("e^x") +local lnx = parse("ln(x)") + +local a = parse("y^2") +local b = parse("x + y + 1") +local c = parse("x*(y+1)+x+3*x*y^2") +local d = parse("x^2+2*x*y+y^2+x") +local e = parse("(x*y+x)^2+x^2") +local f = parse("-x^2/e^x-2*x/e^x-2/e^x+x^2*e^x-2*x*e^x+2*e^x") +local g = parse("x^(-2)+y*x^(-2)+z*x^2+2*x^2") +local h = parse("a*ln(x)-ln(x)*x-x") + + +starttest("collect method") + +testeq(a:collect(x), parse("y^2"), a) +testeq(b:collect(x), parse("x + y + 1"), b) +testeq(c:collect(x), parse("(3*y^2+y+2)*x"), c) +testeq(d:collect(x), parse("x^2+(2*y+1)*x+y^2"), d) +testeq(e:collect(x), parse("((y+1)^2+1)*x^2"), e) +testeq(f:collect(ex), parse("(x^2-2*x+2)*e^x+(-x^2-2*x-2)/e^x"), f) +testeq(g:collect(x), parse("(y+1)*x^(-2)+(z+2)*x^2"), g) +testeq(h:collect(lnx), parse("(a-x)*ln(x)-x"), h) + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/equations.lua b/macros/luatex/latex/luacas/tex/test/expressions/equations.lua new file mode 100644 index 0000000000..3d6aa131fc --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/equations.lua @@ -0,0 +1,17 @@ +local a = Equation(parse("2^x"), parse("1")) +local b = Equation(parse("x^2+2*x+1"), parse("0")) +local c = Equation(parse("2*x^x"), parse("3*y")) +local d = Equation(parse("e^x+1"), parse("y")) +local e = Equation(parse("z*sin(x/2)"), parse("4")) +local f = Equation(parse("4"), parse("0")) + + +starttest("equation solving") +testeq(a:solvefor(parse("x")), Equation(parse("x"), parse("0")), a) +testeq(b:solvefor(parse("x")), Equation(parse("x"), parse("-1")), b) -- This will need to be fixed once set expressions are woring +testeq(c:solvefor(parse("x")), Equation(parse("x^x"), parse("3/2*y")), c) +testeq(c:solvefor(parse("y")), Equation(parse("y"), parse("2/3*x^x")), c) +testeq(d:solvefor(parse("x")), Equation(parse("x"), parse("ln(y - 1)")), d) +testeq(e:solvefor(parse("x")), Equation(parse("x"), parse("2*arcsin(4/z)")), e) +testeq(f:autosimplify(), "false", f) -- Same, with boolean expressions +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/functions.lua b/macros/luatex/latex/luacas/tex/test/expressions/functions.lua new file mode 100644 index 0000000000..e2c15096d6 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/functions.lua @@ -0,0 +1,17 @@ +local a = FunctionExpression("f", + {SymbolExpression("x"), + BinaryOperation.MULEXP + ({SymbolExpression("x"), + Integer(2)})}) + +local b = BinaryOperation.ADDEXP + ({FunctionExpression("g", + {SymbolExpression("x")}), + FunctionExpression("f", + {SymbolExpression("x")}), + Integer(4)}) + +starttest("function expressions") +testeq(a:autosimplify(), parse("f(x, (2 * x))"), a) +testeq(b:autosimplify(), parse("(4 + f(x) + g(x))"), b) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua b/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua new file mode 100644 index 0000000000..2813a6b8f5 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua @@ -0,0 +1,26 @@ +local a = LN(SymbolExpression("x")) +local b = LN(BinaryOperation.POWEXP({E, SymbolExpression("x")})) +local c = BinaryOperation.POWEXP({Integer(2), LOG(Integer(2), SymbolExpression("y"))}) +local d = dparse("e^(-x*ln(x))") + +local e = Logarithm(Integer(2), Integer(256)) +local f = Logarithm(Integer(4), Integer(8)) +local g = Logarithm(Integer(1)/Integer(5), Integer((125))) +local h = Logarithm(Integer(1)/Integer(9), Integer(1)/Integer(243)) +local i = Logarithm(Integer(1)/Integer(25), Integer(3125)) + +local k = Logarithm(E, Integer(1)/Integer(9)) + +starttest("logarithms") +testeq(a, "log(e, x)") +testeq(a:autosimplify(), "log(e, x)", a) +testeq(b:autosimplify(), "x", b) +testeq(c:autosimplify(), "y", c) +testeq(d:autosimplify(), parse("x^(-x)"), d) +testeq(e:autosimplify(), parse("8"), e) +testeq(f:autosimplify(), parse("3/2"), f) +testeq(g:autosimplify(), parse("-3"), g) +testeq(h:autosimplify(), parse("5/2"), h) +testeq(i:autosimplify(), parse("-5/2"), i) +testeq(k:autosimplify(), parse("-ln(9)"), k) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua b/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua new file mode 100644 index 0000000000..85690f8c66 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua @@ -0,0 +1,15 @@ +local a = BinaryOperation.POWEXP({Integer(8), Integer(1) / Integer(2)}) +local b = BinaryOperation.POWEXP({Integer(27), Integer(1) / Integer(3)}) +local c = BinaryOperation.POWEXP({Integer(36), Integer(1) / Integer(2)}) +local d = BinaryOperation.POWEXP({Integer(36264691), Integer(1) / Integer(2)}) +local e = BinaryOperation.POWEXP({Integer(357911), Integer(1) / Integer(2)}) +local f = BinaryOperation.ADDEXP({BinaryOperation.POWEXP({Integer(8), Integer(1) / Integer(2)}), BinaryOperation.POWEXP({Integer(32), Integer(1) / Integer(2)})}) + +starttest("rational powers") +testeq(a:autosimplify(), "(2 * (2 ^ 1/2))", a) +testeq(b:autosimplify(), "3", b) +testeq(c:autosimplify(), "6", c) +testeq(d:autosimplify(), "(331 * (331 ^ 1/2))", d) +testeq(e:autosimplify(), "(71 * (71 ^ 1/2))", e) +testeq(f:autosimplify(), "(6 * (2 ^ 1/2))", f) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua b/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua new file mode 100644 index 0000000000..ac5582901d --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua @@ -0,0 +1,22 @@ +local a = SymbolExpression("x")*(SymbolExpression("y") + SymbolExpression("z")) +local b = SymbolExpression("x")*(Integer(1)+ SymbolExpression("z")) +local c = parse("((2*x+1)*(3*x-1)+6)*(6*y-z)") +local d = parse("(x+1)*(x+2)*(x+3)") + +local e = parse("x*y*z+x^2") +local f = parse("x + 1/x^2") +local g = parse("e^x - e^x*x^2") + +starttest("expression expansion") +testeq(a:expand(), parse("((x * y) + (x * z))"), a) +testeq(b:expand(), parse("(x + (x * z))"), b) +testeq(c:expand(), parse("((30 * y) + (6 * x * y) + (36 * (x ^ 2) * y) + (-5 * z) + (-1 * x * z) + (-6 * (x ^ 2) * z))"), c) +testeq(d:expand(), parse("(6 + (11 * x) + (6 * (x ^ 2)) + (x ^ 3))")) +endtest() + +starttest("expression factoring beyond monovariate polynomials") +testeq(e:factor(), parse("(x * (x + (y * z)))")) +testeq(f:factor(), parse("((x ^ -2) * (1 + x) * (1 + (-1 * x) + (x ^ 2)))")) +testeq(g:factor(), parse("((e ^ x) * (1 + x) * (1 + (-1 * x)))")) + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua b/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua new file mode 100644 index 0000000000..4d1f3496bf --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua @@ -0,0 +1,10 @@ +local a = parse("3*(x+1)^1/2-6*y+3*z^2") +local b = parse("sin(e^x - 1) + e^x") + +starttest("substitution") +testeq(a:substitute({[parse("x")] = Integer(3), + [parse("y")] = Integer(-1), + [parse("z")] = Integer(4)/Integer(3)}):autosimplify(), parse("52/3")) + +testeq(b:substitute({[parse("e^x")] = parse("x^e")}), parse("((x ^ e) + sin((-1 + (x ^ e))))")) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/helper.lua b/macros/luatex/latex/luacas/tex/test/helper.lua new file mode 100644 index 0000000000..b550967da9 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/helper.lua @@ -0,0 +1,322 @@ +-- helper functions + + + function whatis(a) + if a == nil then + return nil + end + if a:type() == SymbolExpression then + return "Sym" + end + if a:type() == BinaryOperation then + return "BinOp" + end + if a:type() == FunctionExpression then + return "FncExp" + end + if a:type() == TrigExpression then + return "TrigExp" + end + if a:type() == Integer then + return "Int" + end + if a:type() == Rational then + return "Ratl" + end + if a:type() == DerivativeExpression then + return "DervExp" + end + if a:type() == DiffExpression then + return "DiffExp" + end + if a:type() == IntegralExpression then + return "Intgrl" + end + if a:type() == SqrtExpression then + return "Sqrt" + end + if a:type() == PolynomialRing then + return "Poly" + end + if a:type() == AbsExpression then + return "ABS" + end + if a:type() == Logarithm then + return "LOG" + end + if a:type() == RootExpression then + return "RootOf" + end + if a:type() == Equation then + return "=" + end + return "No Clue" +end + +function longwhatis(a) + if a == nil then + return nil + end + if a:type() == SymbolExpression then + return "SymbolExpression" + end + if a:type() == BinaryOperation then + return "BinaryOperation" + end + if a:type() == FunctionExpression then + return "FunctionExpression" + end + if a:type() == TrigExpression then + return "TrigExpression" + end + if a:type() == Integer then + return "Integer" + end + if a:type() == Rational then + return "Rational" + end + if a:type() == DerivativeExpression then + return "DerivativeExpression" + end + if a:type() == DiffExpression then + return "DiffExpression" + end + if a:type() == IntegralExpression then + return "IntegralExpression" + end + if a:type() == SqrtExpression then + return "SqrtExpression" + end + if a:type() == PolynomialRing then + return "PolynomialRing" + end + if a:type() == AbsExpression then + return "AbsExpression" + end + if a:type() == Logarithm then + return "Logarithm" + end + if a:type() == RootExpression then + return "RootExpression" + end + if a:type() == Equation then + return "Equation" + end + return "No Clue" +end + +function whatring(a) + if a:getring() == Rational.makering() then + return "Rational" + end + if a:getring() == PolynomialRing.makering() then + return "PolynomialRing" + end + if a:getring() == Integer.makering() then + return "Integer" + end + if a:getring() == IntegerModN.makering() then + return "IntegerModN" + end + return "No Clue" +end + +function nameof(sym) + if sym == nil then + return nil + end + if sym:type() == BinaryOperation then + local binops = {BinaryOperation.ADD, + BinaryOperation.MUL, + BinaryOperation.SUB, + BinaryOperation.DIV, + BinaryOperation.POW, + BinaryOperation.IDIV, + BinaryOperation.MOD} + local obslab = {"ADD", + "MUL", + "SUB", + "DIV", + "POW", + "IDIV", + "MOD"} + for i,j in pairs(binops) do + if sym.operation == j then + return obslab[i] + end + end + end + if sym:type() == FunctionExpression or sym:type() == TrigExpression then + return tostring(sym.name) + end + if sym:type() == SymbolExpression or sym:type() == Integer then + return tostring(sym) + end + if sym:type() == Rational then + return tostring(sym.numerator).."/"..tostring(sym.denominator) + end + if sym:type() == DerivativeExpression then + return "DD" + end + if sym:type() == DiffExpression then + return "diff" + end + if sym:type() == IntegralExpression then + return "$\\mathtt{\\int}$" + end + if sym:type() == SqrtExpression then + return "$\\mathtt{\\sqrt{\\phantom{x}}}$" + end + if sym:type() == PolynomialRing then + return "Poly" + end + if sym:type() == AbsExpression then + return "abs" + end + if sym:type() == Logarithm then + return "log" + end + if sym:type() == RootExpression then + return "RootOf" + end + if sym:type() == Equation then + return "$\\mathtt{=}$" + end + return "No Clue" +end + +function Expression:getfullsubexpressionsrec() + local result = {} + for _, expression in ipairs(self:subexpressions()) do + result[#result+1] = expression + result = JoinArrays(result, expression:getfullsubexpressionsrec()) + end + return result +end + +function Expression:gettheshrub() + local string = "" + for index, expression in ipairs(self:subexpressions()) do + string = string.."child {node [label=-90:{expr["..tostring(index).."]}] {$\\mathtt{"..expression:tolatex().."}$}}" + end + return string +end + +function Expression:getthetree() + local string = "" + for _, expression in ipairs(self:subexpressions()) do + if expression:isatomic() then + string = string.."child {node{"..nameof(expression).."}}" + else + string = string.."child {node{"..nameof(expression).."}"..expression:getthetree().."}" + end + end + return string +end + +function Expression:gettheforest() + local string = "" + for _, expression in ipairs(self:subexpressions()) do + if expression:isatomic() then + string = string.." [ "..nameof(expression).." ] " + else + string = string.." [ "..nameof(expression)..expression:gettheforest().." ] " + end + end + return string +end + +function Expression:getthefancyshrub() + local string = "" + if self:type() == DiffExpression then + for _, expression in ipairs(self:subexpressions()) do + string = string.." [ $\\mathtt{"..expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] " + end + string = string.." [ $\\mathtt{\\{" + for _,symbol in ipairs(self.symbols) do + if next(self.symbols,_) == nil then + string = string .. symbol:tolatex().."\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.symbols};} ] " + else + string = string .. symbol:tolatex() .. "," + end + end + return string + end + if self:type() == IntegralExpression then + string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] " + string = string .. "[ $\\mathtt{"..self.symbol:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.symbol};} ]" + if self:isdefinite() then + string = string .. "[ $\\mathtt{"..self.lower:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.lower};} ] " + string = string .. "[ $\\mathtt{"..self.upper:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.upper};} ] " + return string + end + return string + end + if self:type() == PolynomialRing then + string = string .. " [ $\\mathtt{\\{" + for index=0, self.degree:asnumber() do + string = string .. tostring(self.coefficients[index]) + if index < self.degree:asnumber() then + string = string .. "," + end + end + string = string .. "\\} }$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.coefficients}; \\node[anchor=south west, font=\\ttfamily\\footnotesize,gray] at (.north west) {.ring "..whatring(self).."};} ]" + string = string .. " [ $\\mathtt{"..self.symbol.. "}$, tikz+={\\node[anchor=north, font=\\ttfamily\\footnotesize,gray] at (.south) {.symbol};} ]" + return string + end + if self:type() == SqrtExpression then + string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]" + string = string .. "[ $\\mathtt{"..self.root:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.root};} ]" + return string + end + if self:type() == TrigExpression then + string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]" + return string + end + if self:type() == AbsExpression then + string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] " + return string + end + if self:type() == Logarithm then + string = string .. " [$\\mathtt{" ..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]" + string = string .. " [$\\mathtt{" ..self.base:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.base};} ]" + return string + end + if self:type() == RootExpression then + string = string .. "[$\\mathtt{" ..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]" + return string + end + if self:type() == FunctionExpression then + local string1 = '' + local string2 = '' + local string3 = '' + for index=1, #self.variables do + string1 = string1 .. tostring(self.expressions[index]) + if index < #self.variables then + string1 = string1 .. "," + end + string2 = string2 .. tostring(self.variables[index]) + if index < #self.variables then + string2 = string2 .. "," + end + string3 = string3 .. tostring(self.derivatives[index]) + if index < #self.variables then + string3 = string3 .. "," + end + end + string = string .. "[$\\mathtt{ \\{" .. string1 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expressions};} ]" + string = string .. "[$\\mathtt{ \\{" .. string2 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.variables};} ]" + string = string .. "[$\\mathtt{ \\{" .. string3 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.derivatives};} ]" + return string + end + if self:type() == Equation then + string = string .. " [$\\mathtt{" ..self.lhs:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.lhs};} ]" + string = string .. " [$\\mathtt{" ..self.rhs:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.rhs};} ]" + return string + end + for index, expression in ipairs(self:subexpressions()) do + string = string.." [ $\\mathtt{"..expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression["..index.."]};} ] " + end + return string +end + diff --git a/macros/luatex/latex/luacas/tex/test/main.lua b/macros/luatex/latex/luacas/tex/test/main.lua new file mode 100644 index 0000000000..638f7734b5 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/main.lua @@ -0,0 +1,154 @@ +---@diagnostic disable: lowercase-global +-- Runs test code from test files. + +require("calculus._init") +require("_lib.pepperfish") + +-- Stuff required for the basic parser. +local constants = {e="E", pi = "PI", ln = "LN", log = "LOG", Integer = "Integer", DD = "DD", int = "INT", abs = "ABS", fact="FACT"} + +local function parser(s) + if string.find(s, "[0-9]+") then + return "Integer(\"" .. s .. "\")" + end + + if s.find(s, "[%^%\\%[%]]") then + return string.gsub(s, "[^%^%\\%[%]]+", parser) + end + + for string, replace in pairs(constants) do + if s == string then + return replace + end + end + + return "SymbolExpression(\"" .. s .. "\")" +end + +function parse(input) + local parsed = string.gsub(input, "[0-9]+", parser) + parsed = string.gsub(parsed, "[A-z']+", parser) + local exe, err = load("return " .. parsed) + if exe then + return exe():autosimplify() + else + print(err) + end +end + +function dparse(input) + local parsed = string.gsub(input, "[0-9]+", parser) + parsed = string.gsub(parsed, "[A-z']+", parser) + local exe, err = load("return " .. parsed) + if exe then + return exe() + else + print(err) + end +end + +-- Stuff required for test code. +local tests +local failures +local totaltests = 0 +local totalfailures = 0 +function starttest(name) + print("Testing " .. name .. "...") + print() + tests = 0 + failures = 0 +end + +-- Tests two objects for equality, irrespective of order. If the object is a table or expression, the objects may be sorted to ensure the correct order. +function testeq(actual, expected, initial, sort) + if sort and type(actual) == "table" and not actual.type then + table.sort(actual, function (a, b) + return a:order(b) + end) + elseif sort and type(actual) == "table" and actual.type and actual:type() == BinaryOperation and actual:iscommutative() then + table.sort(actual.expressions, function (a, b) + return a:order(b) + end) + end + + if initial then + if ToStringArray(expected) == ToStringArray(actual) then + print(ToStringArray(initial) .. " -> " .. ToStringArray(actual)) + else + print(ToStringArray(initial) .. " -> " .. ToStringArray(actual) .. " (Expected: " .. ToStringArray(expected) .. ")") + failures = failures + 1 + end + else + if ToStringArray(expected) == ToStringArray(actual) then + print("Result: " .. ToStringArray(actual)) + else + print("Result: ".. ToStringArray(actual) .. " (Expected: " .. ToStringArray(expected) .. ")") + failures = failures + 1 + end + end + tests = tests + 1 +end + +-- Tests whether converting an element to a different ring produces the expected object in the expected ring +function testringconvert(expression, toring, expected, expectedring) + testeq(expression:inring(toring), expected, expression) + testeq(expression:inring(toring):getring(), expectedring) +end + +function endtest() + print() + print("Finished test without errors.") + print() + totaltests = totaltests + tests + totalfailures = totalfailures + failures + if failures == 0 then + print("Performed " .. tests .. " tests, all of which passed!") + else + print("Performed tests, " .. failures .. "/" .. tests .. " failed.") + end + print("=====================================================================================================================") +end + +function endall() + if totalfailures == 0 then + print("Performed " .. totaltests .. " tests in total, all of which passed!") + else + print("Performed tests, " .. totalfailures .. "/" .. totaltests .. " failed.") + end +end + + +-- TODO: Add profiling and error catching options. +-- Comment out these lines to only run certain test code. + +-- profiler = newProfiler() +-- profiler:start() + +require("test.calculus.derivatives") +require("test.calculus.integrals") + +require("test.expressions.autosimplify") +require("test.expressions.collect") +require("test.expressions.equations") +require("test.expressions.simplify") +require("test.expressions.functions") +require("test.expressions.logarithms") +-- require("test.expressions.rationalexponent") +require("test.expressions.substitute") + +require("test.polynomials.polynomial") +require("test.polynomials.partialfractions") +require("test.polynomials.polynomialmod") +require("test.polynomials.roots") + +require("test.rings.conversion") +require("test.rings.modulararithmetic") +require("test.rings.number") + +endall() + +-- profiler:stop() + +-- local outfile = io.open( "profile.txt", "w+" ) +-- profiler:report( outfile ) +-- outfile:close()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/parser.lua b/macros/luatex/latex/luacas/tex/test/parser.lua new file mode 100644 index 0000000000..6cb22a1bdb --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/parser.lua @@ -0,0 +1,323 @@ +-- Rudimentary parser for making the CAS easier to use. Essentially just wraps SymbolExpression() around symbols and Integer() around integers. + + + +require("calculus._init") + +-- Splits a string on a seperator. +function split(str, sep) + local t={} + for match in string.gmatch(str, "([^".. sep .."]+)") do + t[#t+1] = match + end + return t +end + +-- Displays an expression. For use in the parser. +function disp(expression, inline, simple) + if type(expression) ~= "table" then + tex.print(tostring(expression)) + elseif expression.autosimplify then + if inline then + if simple then + tex.print('$' .. expression:autosimplify():tolatex() .. '$') + else + tex.print('$' .. expression:tolatex() .. '$') + end + else + if simple then + tex.print('\\[' .. expression:autosimplify():tolatex() .. '\\]') + else + tex.print('\\[' .. expression:tolatex() .. '\\]') + end + end + else + tex.print(tostring(expression)) + end +end + +-- Displays an expression. For use in the parser. +function displua(expression) + if type(expression) ~= "table" then + print(tostring(expression)) + elseif expression.autosimplify then + print(expression:autosimplify():tolatex()) + else + print(tostring(expression)) + end +end + +function vars(...) + for _, string in ipairs(table.pack(...)) do + if string ~= "_" then + _G[string] = SymbolExpression(string) + end + end +end + +function clearvars() + for index, value in pairs(_G) do + if type(value) == "table" and value.type and value:type() == SymbolExpression then + _G[index] = nil + end + end +end + +function range(a, b, step) + if not b then + b = a + a = Integer.one() + end + step = step or Integer.one() + local f = + step > Integer.zero() and + function(_, lastvalue) + local nextvalue = lastvalue + step + if nextvalue <= b then return nextvalue end + end or + step < Integer.zero() and + function(_, lastvalue) + local nextvalue = lastvalue + step + if nextvalue >= b then return nextvalue end + end or + function(_, lastvalue) return lastvalue end + return f, nil, a - step + end + +function factor(exp,squarefrei) + if exp:type() == Integer then + return exp:primefactorization() + end + if exp:type() == PolynomialRing then + if not squarefrei then + return exp:factor() + else + if exp.ring == Integer.getring() or Rational.getring() then + return exp:squarefreefactorization() + end + if exp.ring == IntegerModN.getring() then + return exp:modularsquarefreefactorization() + end + return exp:factor() + end + end + return exp:autosimplify():factor() +end + +function expand(exp) + return exp:autosimplify():expand() +end + +function simplify(exp) + return exp:simplify() +end + +function exp(x) + return e^x +end + +function substitute(tbl,expr) + return expr:substitute(tbl) +end + +function roots(expression) + poly,ispoly = topoly(expression) + if ispoly then + return poly:roots() + end + return RootExpression(expression) +end + +function combine(expr) + return expr:combine() +end + +function Mod(f,n) + if f:type() == Integer then + return IntegerModN(f,n) + end + if f:type() == PolynomialRing and f.ring == Integer.getring() then + local coeffs = {} + for i=0,f.degree:asnumber() do + coeffs[i] = IntegerModN(f.coefficients[i],n) + end + return PolynomialRing(coeffs,f.symbol,f.degree) + end +end + +function Poly(coefficients,symbol,degree) + local variable = symbol or 'x' + return PolynomialRing:new(coefficients,variable,degree) +end + +function topoly(a) + a = a:expand():autosimplify() + return a:topolynomial() +end + +function gcd(a,b) + if a:type() == Integer and b:type() == Integer then + return Integer.gcd(a,b) + end + if a:type() == PolynomialRing and b:type() == PolynomialRing then + return PolynomialRing.gcd(a,b) + end +end + +function gcdext(a,b) + if a:type() == Integer and b:type() == Integer then + return Integer.extendedgcd(a,b) + end + A, ATF = topoly(a) + B, BTF = topoly(b) + if ATF and BTF then + return PolynomialRing.extendedgcd(A,B) + end + return nil,nil,nil +end + +function parfrac(f,g,ffactor) + local f,check1 = topoly(f) + local g,check2 = topoly(g) + if check1 and check2 then + if f.degree >= g.degree then + local q,r + q,r = f:divremainder(g) + return q + PolynomialRing.partialfractions(r,g,ffactor) + else + return PolynomialRing.partialfractions(f,g,ffactor) + end + else + return f/g + end +end + +function factorial(a) + return FactorialExpression(a) +end + +-- Constants for the CAS. We may not want these in Lua itself, but in the latex end the user probably expects them. +e = E +pi = PI +-- sqrt = SQRT +ln = LN +log = LOG +int = INT +sin = SIN +cos = COS +tan = TAN +csc = CSC +sec = SEC +cot = COT +arcsin = ARCSIN +arccos = ARCCOS +arctan = ARCTAN +arccsc = ARCCSC +arcsec = ARCSEC +arccot = ARCCOT +abs = ABS + +function ZTable(t) + t = t or {} + return setmetatable(t, JoinTables(getmetatable(t), + {__index = function (t, k) + if type(k) == "table" and k.type and k:type() == Integer then + return rawget(t, k:asnumber()) + else + return rawget(t, k) + end + end, + __newindex = function (t, k, v) + if type(k) == "table" and k.type and k:type() == Integer then + rawset(t, k:asnumber(), v) + else + rawset(t, k, v) + end + end})) +end + +function RR(n) + if type(n) == "number" then + return n + end + + if type(n) == "string" then + return tonumber(n) + end + + if type(n) == "table" and n.asnumber then + return n:asnumber() + end + + error("Could not convert to a real number.") +end + +function ZZ(n) + if type(n) == "table" and n.type and n:type() == Rational then + return n.numerator // n.denominator + end + return Integer(n) +end + +function QQ(n) + if type(n) == "table" then + return n + end + + if type(n) == "number" then + n = tostring(n) + end + + if type(n) == "string" then + local parts = split(n, "%.") + if #parts == 1 then + return Integer(parts[1]) + else + return Integer(parts[1])..Integer(parts[2]) + end + end + + error("Could not convert to a rational number.") +end + +--- Parses raw input into Lua code and executes it. +--- @param input string +function CASparse(input) + + -- First, we replace any occurance of a number with an integer or rational version of itself. + local str = string.gsub(input, ".?[0-9]+", function (s) + -- Here, we are part of an identifier, so we don't replace anything + if string.match(string.sub(s, 1, 1), "[A-Z]") or string.match(string.sub(s, 1, 1), "[a-z]") or string.match(string.sub(s, 1, 1), "_") then + return + end + + if string.match(string.sub(s, 1, 1), "[0-9]") then + return "Integer('" .. s .. "')" + end + + return string.sub(s, 1, 1) .. "Integer('" .. string.sub(s, 2, #s) .. "')" + end) + + -------------------------- + -- HERE COMES THE JANK. -- + -------------------------- + + -- Replaces each instance of a decimal with .., so we can use integer metatables to convert it into a rational properly. + str = string.gsub(str, "Integer%('[0-9]+'%)%.Integer%('[0-9]+'%)", function (s) + local ints = split(s, "%.") + return ints[1] .. ".." .. ints[2] + end) + str = string.gsub(str, ".?%.Integer%('[0-9]+'%)", function (s) + if string.sub(s, 1, 2) == ".." then + return + end + return string.sub(s, 1, 1) .. "Integer('0')." .. string.sub(s, 2, #s) + end) + + local exe, err = load(str .. "\n return true") + if exe then + exe() + else + print(err) + end +end
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua b/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua new file mode 100644 index 0000000000..280d4d56f3 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua @@ -0,0 +1,12 @@ +local g1 = parse("x^3+4*x^2-x-2"):topolynomial() +local f1 = parse("x^4-x^2"):topolynomial() + +local g2 = parse("2*x^6-4*x^5+5*x^4-3*x^3+x^2+3*x"):topolynomial() +local f2 = parse("x^7-3*x^6+5*x^5-7*x^4+7*x^3-5*x^2+3*x-1"):topolynomial() + +starttest("partial fraction decomposition") + +testeq(PolynomialRing.partialfractions(g1, f1):autosimplify(), parse("((2 * (x ^ -2)) + (x ^ -1) + ((-1 + x) ^ -1) + (-1 * ((1 + x) ^ -1)))")) +testeq(PolynomialRing.partialfractions(g2, f2):autosimplify(), parse("(((-1 + x) ^ -3) + ((-1 + x) ^ -1) + ((1 + (x ^ 2)) ^ -2) + ((1 + x) * ((1 + (x ^ 2)) ^ -1)))")) + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua b/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua new file mode 100644 index 0000000000..545538c764 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua @@ -0,0 +1,153 @@ + +local a = PolynomialRing({ + Integer(1), + Integer(2), + Integer(3), + Integer(4), + Integer(5) +}, "x") + +local b = PolynomialRing({ + Integer(1) / Integer(3), + Integer(1) / Integer(12), + Integer(6) / Integer(3), +}, "x") + +local c = PolynomialRing({ + Integer(12), + Integer(4) +}, "x") + +local h = PolynomialRing({Integer(2), Integer(3), Integer(1)}, "x") +local i = PolynomialRing({Integer(8), Integer(20), Integer(18), Integer(7), Integer(1)}, "x") +local j = PolynomialRing({Integer(108), Integer(324), Integer(387), Integer(238), Integer(80), Integer(14), Integer(1)}, "x") +local k = PolynomialRing({Integer(30), Integer(11), Integer(1)}, "x") +local l = PolynomialRing({Integer(6), Integer(11), Integer(6), Integer(1)}, "z") +local m = PolynomialRing({Integer(1), Integer(10), Integer(45), Integer(120), Integer(210), Integer(252), Integer(210), Integer(120), Integer(45), Integer(10), Integer(1)}, "x") +local n = PolynomialRing({Integer(24), Integer(50), Integer(35), Integer(10), Integer(1), Integer(0), Integer(24), Integer(50), Integer(35), Integer(10), Integer(1)}, "x") +local o = PolynomialRing({Integer(24), Integer(50), Integer(59), Integer(60), Integer(36), Integer(10), Integer(1)}, "x") +local p = PolynomialRing({Integer(-110592), Integer(59904), Integer(-5760), Integer(720), Integer(-48), Integer(1)}, "x") + +local d = PolynomialRing({Integer(21), Integer(10), Integer(1)}, "x") +local e = PolynomialRing({Integer(-6), Integer(1), Integer(1)}, "x") + +local f = PolynomialRing({Integer(-1), Integer(-2), Integer(15), Integer(36)}, "x") +local g = PolynomialRing({Integer(1), Integer(7), Integer(15), Integer(9)}, "x") + +local q = PolynomialRing({Integer(3), Integer(-9), Integer(27), Integer(-36), Integer(36)}, "z") +local r = PolynomialRing({Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(4)}, "x"); +local s = PolynomialRing({Integer(1), Integer(0), Integer(-4), Integer(0), Integer(1)}, "x") + +local x = Integer(3) +local y = Integer(-1) / Integer(6) + +local multia = PolynomialRing({Integer(4), + Integer(0), + PolynomialRing({Integer(0), Integer(0), Integer(-6)}, "y"), + PolynomialRing({Integer(1), Integer(3)}, "y")}, "x") +local multib = PolynomialRing({PolynomialRing({Integer(0), Integer(6)}, "y"), + Integer(0), + PolynomialRing({Integer(-4), Integer(12)}, "y")}, "x") + +starttest("polynomial construction") +testeq(a, "5x^4+4x^3+3x^2+2x^1+1x^0") +testeq(a.degree, 4) +testeq(b, "2x^2+1/12x^1+1/3x^0") +testeq(b.degree, 2) +testeq(multia, "(3y^1+1y^0)x^3+(-6y^2+0y^1+0y^0)x^2+(0)x^1+(4)x^0") +testeq(multia.degree, 3) +endtest() + +starttest("polynomial-expression conversion") +testeq(a:tocompoundexpression():autosimplify():topolynomial(), a) +testeq(b:tocompoundexpression():autosimplify():topolynomial(), b) +testeq(c:tocompoundexpression():autosimplify():topolynomial(), c) +endtest() + +starttest("polynomial arithmetic") +testeq(a + a, "10x^4+8x^3+6x^2+4x^1+2x^0") +testeq(a + b, "5x^4+4x^3+5x^2+25/12x^1+4/3x^0") +testeq(b + a, "5x^4+4x^3+5x^2+25/12x^1+4/3x^0") +testeq(a - a, "0x^0") +testeq(a - b, "5x^4+4x^3+1x^2+23/12x^1+2/3x^0") +testeq(b:multiplyDegree(4), "2x^6+1/12x^5+1/3x^4+0x^3+0x^2+0x^1+0x^0") +testeq(a:multiplyDegree(12), "5x^16+4x^15+3x^14+2x^13+1x^12+0x^11+0x^10+0x^9+0x^8+0x^7+0x^6+0x^5+0x^4+0x^3+0x^2+0x^1+0x^0") +testeq(c * c, "16x^2+96x^1+144x^0") +testeq(a * c, "20x^5+76x^4+60x^3+44x^2+28x^1+12x^0") +testeq(c * a, "20x^5+76x^4+60x^3+44x^2+28x^1+12x^0") +testeq(b * c, "8x^3+73/3x^2+7/3x^1+4x^0") +local qq, rr = a:divremainder(c) +testeq(qq, "5/4x^3+-11/4x^2+9x^1+-53/2x^0") +testeq(rr, "319x^0") +qq, rr = a:divremainder(b) +testeq(qq, "5/2x^2+91/48x^1+1157/1152x^0") +testeq(rr, "17755/13824x^1+2299/3456x^0") +endtest() + +starttest("polynomial pseudodivision") +local pq, pr = a:pseudodivide(c) +testeq(pq, "320x^3+-704x^2+2304x^1+-6784x^0") +testeq(pr, "81664x^0") + +pq, pr = multia:pseudodivide(multib) +testeq(pq, "(36y^2+0y^1+-4y^0)x^1+(-72y^3+24y^2+0y^1+0y^0)x^0") +testeq(pr, "(-216y^3+0y^2+24y^1+0y^0)x^1+(432y^4+-144y^3+576y^2+-384y^1+64y^0)x^0") +endtest() + + +starttest("combined polynomial/coefficient operations") +testeq(a + x, "5x^4+4x^3+3x^2+2x^1+4x^0") +testeq(x + a, "5x^4+4x^3+3x^2+2x^1+4x^0") +testeq(b - y, "2x^2+1/12x^1+1/2x^0") +testeq(a * x, "15x^4+12x^3+9x^2+6x^1+3x^0") +testeq(x * a, "15x^4+12x^3+9x^2+6x^1+3x^0") +endtest() + +starttest("polynomial formal derivatives") +testeq(a:derivative(), "20x^3+12x^2+6x^1+2x^0") +testeq(b:derivative(), "4x^1+1/12x^0") +testeq(c:derivative():derivative(), "0x^0") +endtest() + + +starttest("polynomial gcd...") +testeq(PolynomialRing.gcd(d, e), "1x^1+3x^0") +testeq(PolynomialRing.gcd(b, c), "1x^0") +testeq(PolynomialRing.gcd(f, g), "1x^2+2/3x^1+1/9x^0") +endtest() + +starttest("square-free factorization") +testeq(h:squarefreefactorization():autosimplify(), parse("(2 + (3 * x) + (x ^ 2))"), h) +testeq(i:squarefreefactorization():autosimplify(), parse("((1 + x) * ((2 + x) ^ 3))"), i) +testeq((Integer(2)*i):squarefreefactorization():autosimplify(), parse("(((2 + x) ^ 3) * (2 + (2 * x)))"), (Integer(2)*i), true) +testeq(j:squarefreefactorization():autosimplify(), parse("((1 + x) * ((2 + x) ^ 2) * ((3 + x) ^ 3))"), j) +testeq(o:squarefreefactorization():autosimplify(), parse("(24 + (50 * x) + (59 * (x ^ 2)) + (60 * (x ^ 3)) + (36 * (x ^ 4)) + (10 * (x ^ 5)) + (x ^ 6))"), o) +endtest() + +starttest("polynomial factorization") +testeq(c:factor(), BinaryOperation.MULEXP({Integer(4), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, "x"), Integer(1)})}), c) +testeq(h:factor():autosimplify(), parse("((1 + x) * (2 + x))"), h) +testeq(k:factor():autosimplify(), parse("((5 + x) * (6 + x))"), k) +testeq(j:factor():autosimplify(), parse("((1 + x) * ((2 + x) ^ 2) * ((3 + x) ^ 3))"), j) +testeq(p:factor():autosimplify(), parse("((-24 + x) * (96 + (x ^ 2)) * (48 + (-24 * x) + (x ^ 2)))"), p) +testeq(l:factor():autosimplify(), parse("((1 + z) * (2 + z) * (3 + z))"), l) +testeq(m:factor():autosimplify(), parse("((1 + x) ^ 10)"), m) +testeq(b:factor(), BinaryOperation.MULEXP({Integer(1)/Integer(12), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1), Integer(24)}, "x"), Integer(1)})}), b) +testeq(o:factor():autosimplify(), parse("((1 + x) * (2 + x) * (3 + x) * (4 + x) * (1 + (x ^ 2)))"), o) +testeq(n:factor():autosimplify(), parse("((1 + x) * (2 + x) * (3 + x) * (4 + x) * (1 + (x ^ 2)) * (1 + (-1 * (x ^ 2)) + (x ^ 4)))"), n) +endtest() + +starttest("polynomial decomposition") +testeq(c:decompose(), "{4x^1+12x^0}", c, true) +testeq(h:decompose(), "{1x^2+3x^1+2x^0}", h, true) +testeq(k:decompose(), "{1x^2+11x^1+30x^0}", k, true) +testeq(j:decompose(), "{1x^6+14x^5+80x^4+238x^3+387x^2+324x^1+108x^0}", j, true) +testeq(l:decompose(), "{1z^3+6z^2+11z^1+6z^0}", l, true) +testeq(m:decompose(), "{1x^5+5x^4+10x^3+10x^2+5x^1+1x^0, 1x^2+2x^1+0x^0}", m, true) +testeq(b:decompose(), "{2x^2+1/12x^1+1/3x^0}", b, true) +testeq(o:decompose(), "{1x^6+10x^5+36x^4+60x^3+59x^2+50x^1+24x^0}", o, true) +testeq(n:decompose(), "{1x^10+10x^9+35x^8+50x^7+24x^6+0x^5+1x^4+10x^3+35x^2+50x^1+24x^0}", n, true) +testeq(q:decompose(), "{36z^2+18z^1+3z^0, 1z^2+-1/2z^1+0z^0}", q, true) +testeq(r:decompose(),"{4x^3+0x^2+0x^1+0x^0, 1x^2+0x^1+0x^0}", r, true) +testeq(s:decompose(), "{1x^2+4x^1+1x^0, 1x^2+0x^1+-4x^0}", s, true) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua b/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua new file mode 100644 index 0000000000..1906ad2d72 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua @@ -0,0 +1,76 @@ +local a = PolynomialRing({IntegerModN(Integer(1), Integer(11)), + IntegerModN(Integer(6), Integer(11)), + IntegerModN(Integer(1), Integer(11)), + IntegerModN(Integer(9), Integer(11)), + IntegerModN(Integer(1), Integer(11))}, "y") + +local b = PolynomialRing({IntegerModN(Integer(7), Integer(11)), + IntegerModN(Integer(7), Integer(11)), + IntegerModN(Integer(6), Integer(11)), + IntegerModN(Integer(2), Integer(11)), + IntegerModN(Integer(1), Integer(11))}, "y") + +local q = PolynomialRing({IntegerModN(Integer(2), Integer(13)), + IntegerModN(Integer(6), Integer(13)), + IntegerModN(Integer(4), Integer(13))}, "z") + +local p = PolynomialRing({IntegerModN(Integer(4), Integer(13)), + IntegerModN(Integer(11), Integer(13)), + IntegerModN(Integer(1), Integer(13)), + IntegerModN(Integer(12), Integer(13)), + IntegerModN(Integer(1), Integer(13))}, "x") + +local r = PolynomialRing({IntegerModN(Integer(1), Integer(3)), + IntegerModN(Integer(0), Integer(3)), + IntegerModN(Integer(0), Integer(3)), + IntegerModN(Integer(2), Integer(3)), + IntegerModN(Integer(0), Integer(3)), + IntegerModN(Integer(0), Integer(3)), + IntegerModN(Integer(1), Integer(3))}, "x") + +local s = PolynomialRing({IntegerModN(Integer(1), Integer(5)), + IntegerModN(Integer(1), Integer(5)), + IntegerModN(Integer(1), Integer(5)), + IntegerModN(Integer(1), Integer(5)), + IntegerModN(Integer(1), Integer(5))}, "x") + +local t = PolynomialRing({IntegerModN(Integer(1), Integer(7))}, "x"):multiplyDegree(7) - PolynomialRing({IntegerModN(Integer(1), Integer(7))}, "x"):multiplyDegree(1) + +local u = PolynomialRing({IntegerModN(Integer(1), Integer(13)), + IntegerModN(Integer(5), Integer(13)), + IntegerModN(Integer(6), Integer(13)), + IntegerModN(Integer(5), Integer(13)), + IntegerModN(Integer(1), Integer(13))}, "x") + +local v = PolynomialRing({IntegerModN(Integer(24), Integer(7)), + IntegerModN(Integer(50), Integer(7)), + IntegerModN(Integer(59), Integer(7)), + IntegerModN(Integer(60), Integer(7)), + IntegerModN(Integer(36), Integer(7)), + IntegerModN(Integer(10), Integer(7)), + IntegerModN(Integer(1), Integer(7))}, "z") + +starttest("modular polynomial operations") +testeq(q*q, "3z^4+9z^3+0z^2+11z^1+4z^0") +testeq(PolynomialRing.gcd(a, b), "1y^0") +local Q, R, S = PolynomialRing.extendedgcd(a, b) +testeq(Q, "1y^0") +testeq(R, "4y^3+5y^2+1y^1+3y^0") +testeq(S, "7y^3+0y^2+7y^1+6y^0") +endtest() + +starttest("modular square free factoring") +testeq(p:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(2), Integer(6), Integer(1)}, SymbolExpression("x"))), Integer(2)})) +testeq(r:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(1), Integer(0), Integer(0), Integer(1)}, SymbolExpression("x"))), Integer(2)})) +testeq(q:squarefreefactorization(), Integer(4) * BinaryOperation.POWEXP({(PolynomialRing({Integer(7), Integer(8), Integer(1)}, SymbolExpression("z"))), Integer(1)})) +testeq(s:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x"))), Integer(4)})) +endtest() + +starttest("modular polynomial factoring") +testeq(q:factor(), BinaryOperation.MULEXP({Integer(4), BinaryOperation.POWEXP({PolynomialRing({Integer(7), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("z")), Integer(1)})}), q) +testeq(p:factor(), Integer(1) * BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(6), Integer(1)}, SymbolExpression("x")), Integer(2)}), p) +testeq(r:factor(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(1), Integer(0), Integer(0), Integer(1)}, SymbolExpression("x"))), Integer(2)}), r) +testeq(t:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(0), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(6), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(5), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("x")), Integer(1)})}), t) +testeq(u:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(11), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(10), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(6), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")), Integer(1)})}), u) +testeq(v:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(0), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("z")), Integer(1)})}), v) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua b/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua new file mode 100644 index 0000000000..01975ab1ec --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua @@ -0,0 +1,43 @@ +local a = PolynomialRing({Integer(1), Integer(2), Integer(3), Integer(4), Integer(5)}, "x") +local b = PolynomialRing({Integer(1) / Integer(3), Integer(1) / Integer(12), Integer(6) / Integer(3)}, "x") +local c = PolynomialRing({Integer(12), Integer(4)}, "x") +local d = PolynomialRing({Integer(21), Integer(10), Integer(1)}, "x") +local e = PolynomialRing({Integer(-6), Integer(1), Integer(1)}, "x") +local f = PolynomialRing({Integer(-1), Integer(-2), Integer(15), Integer(36)}, "x") +local g = PolynomialRing({Integer(1), Integer(7), Integer(15), Integer(9)}, "x") +local h = PolynomialRing({Integer(2), Integer(3), Integer(1)}, "x") +local i = PolynomialRing({Integer(8), Integer(20), Integer(18), Integer(7), Integer(1)}, "x") +local j = PolynomialRing({Integer(108), Integer(324), Integer(387), Integer(238), Integer(80), Integer(14), Integer(1)}, "x") +local k = PolynomialRing({Integer(30), Integer(11), Integer(1)}, "x") +local l = PolynomialRing({Integer(6), Integer(11), Integer(6), Integer(1)}, "z") +local m = PolynomialRing({Integer(1), Integer(10), Integer(45), Integer(120), Integer(210), Integer(252), Integer(210), Integer(120), Integer(45), Integer(10), Integer(1)}, "x") +local n = PolynomialRing({Integer(24), Integer(50), Integer(35), Integer(10), Integer(1), Integer(0), Integer(24), Integer(50), Integer(35), Integer(10), Integer(1)}, "x") +local o = PolynomialRing({Integer(24), Integer(50), Integer(59), Integer(60), Integer(36), Integer(10), Integer(1)}, "x") +local p = PolynomialRing({Integer(-110592), Integer(59904), Integer(-5760), Integer(720), Integer(-48), Integer(1)}, "x") +local q = PolynomialRing({Integer(3), Integer(-9), Integer(27), Integer(-36), Integer(36)}, "z") +local r = PolynomialRing({Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(4)}, "x") +local s = PolynomialRing({Integer(1), Integer(0), Integer(-4), Integer(0), Integer(1)}, "x") +local t = PolynomialRing({Integer(1), Integer(-1), Integer(1), Integer(1)}, "t") + +starttest("polynomial root-finding") +testeq(a:roots(), "{Root Of: (5x^4+4x^3+3x^2+2x^1+1x^0)}", a, true) +testeq(b:roots(), "{-1/48 + (-1/48 * (383 ^ (1/2)) * i), -1/48 + (1/48 * (383 ^ (1/2)) * i)}", b, true) +testeq(c:roots(), "{-3}", c, true) +testeq(d:roots(), "{-7, -3}", d, true) +testeq(e:roots(), "{-3, 2}", e, true) +testeq(f:roots(), "{-1/3, 1/4}", f, true) +testeq(g:roots(), "{-1, -1/3}", g, true) +testeq(h:roots(), "{-2, -1}", h, true) +testeq(i:roots(), "{-2, -1}", i, true) +testeq(j:roots(), "{-3, -2, -1}", j, true) +testeq(k:roots(), "{-6, -5}", k, true) +testeq(l:roots(), "{-3, -2, -1}", l, true) +testeq(m:roots(), "{-1}", m, true) +testeq(n:roots(), "{-4, -3, -2, -1, i, -1 * i, -1/2 * ((2 + (-2 * (3 ^ (1/2)) * i)) ^ (1/2)), 1/2 * ((2 + (-2 * (3 ^ (1/2)) * i)) ^ (1/2)), -1/2 * ((2 + (2 * (3 ^ (1/2)) * i)) ^ (1/2)), 1/2 * ((2 + (2 * (3 ^ (1/2)) * i)) ^ (1/2))}", n, true) +testeq(o:roots(), "{-4, -3, -2, -1, i, -1 * i}", o, true) +testeq(p:roots(), "{24, 12 + (-4 * (6 ^ (1/2))), 12 + (4 * (6 ^ (1/2))), -4 * (6 ^ (1/2)) * i, 4 * (6 ^ (1/2)) * i}", p, true) +testeq(q:roots(), "{1/4 + (-1/2 * ((-3/4 + (-1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (1/2 * ((-3/4 + (-1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (-1/2 * ((-3/4 + (1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (1/2 * ((-3/4 + (1/3 * (3 ^ (1/2)) * i)) ^ (1/2)))}", q, true) +testeq(r:roots(), "{0}", r, true) +testeq(s:roots(), "{-1/2 * ((8 + (-4 * (3 ^ (1/2)))) ^ (1/2)), 1/2 * ((8 + (-4 * (3 ^ (1/2)))) ^ (1/2)), -1/2 * ((8 + (4 * (3 ^ (1/2)))) ^ (1/2)), 1/2 * ((8 + (4 * (3 ^ (1/2)))) ^ (1/2))}", s, true) +testeq(t:roots(), "{-1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3))) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3))), -1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ -1)) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3)) * (-1/2 + (1/2 * (3 ^ (1/2)) * i))), -1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ -2)) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ 2))}", t, true) +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/rings/conversion.lua b/macros/luatex/latex/luacas/tex/test/rings/conversion.lua new file mode 100644 index 0000000000..b9eb523c7a --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/rings/conversion.lua @@ -0,0 +1,273 @@ +local a = Integer(12) +local b = Integer(3) / Integer(2) +local c = IntegerModN(Integer(4), Integer(7)) +local d = IntegerModN(Integer(8), Integer(14)) +local e = PolynomialRing({Integer(6), Integer(0), Integer(3)}, SymbolExpression("x")) +local f = PolynomialRing({Integer(4)/Integer(5), Integer(12)}, SymbolExpression("x")) +local g = PolynomialRing({ + PolynomialRing({ + PolynomialRing({Integer(-4), Integer(12), Integer(1)}, SymbolExpression("x")), + PolynomialRing({Integer(0)}, SymbolExpression("x")), + PolynomialRing({Integer(8)}, SymbolExpression("x")) + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(8), Integer(7), Integer(6), Integer(5)}, SymbolExpression("x")), + PolynomialRing({Integer(-1)}, SymbolExpression("x")), + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(0)}, SymbolExpression("x")), + PolynomialRing({Integer(2), Integer(8), Integer(1)}, SymbolExpression("x")), + PolynomialRing({Integer(-16), Integer(4)}, SymbolExpression("x")), + PolynomialRing({Integer(1)}, SymbolExpression("x")) + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(1)}, SymbolExpression("x")) + }, + SymbolExpression("y")) + }, SymbolExpression("z")) +local h = PolynomialRing({ + PolynomialRing({ + PolynomialRing({Integer(-4), Integer(4)/Integer(5), Integer(1)}, SymbolExpression("z")), + PolynomialRing({Integer(0)}, SymbolExpression("z")), + PolynomialRing({Integer(8)}, SymbolExpression("z")) + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(8), Integer(7), Integer(6), Integer(5)}, SymbolExpression("z")), + PolynomialRing({Integer(-1)}, SymbolExpression("z")), + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(0)}, SymbolExpression("z")), + PolynomialRing({Integer(2), Integer(8), Integer(1)}, SymbolExpression("z")), + PolynomialRing({Integer(-16), Integer(1)/Integer(9)}, SymbolExpression("z")), + PolynomialRing({Integer(1)}, SymbolExpression("z")) + }, + SymbolExpression("y")), + PolynomialRing({ + PolynomialRing({Integer(1)}, SymbolExpression("z")) + }, + SymbolExpression("y")) + }, SymbolExpression("x")) +local i = Rational(PolynomialRing({-Integer(2), Integer(1)}, SymbolExpression("x")), + PolynomialRing({Integer(3), Integer(3), Integer(1)}, SymbolExpression("x"))) +local j = Rational(PolynomialRing({-Integer(2), Integer(1)}, SymbolExpression("x")), + PolynomialRing({Integer(3)/Integer(4), Integer(3)/Integer(8), Integer(1)}, SymbolExpression("x"))) +local k = PolynomialRing({IntegerModN(Integer(0), Integer(5)), + IntegerModN(Integer(1), Integer(5)), + IntegerModN(Integer(3), Integer(5)), + IntegerModN(Integer(1), Integer(5))}, SymbolExpression("x")) + +local l = PolynomialRing({Rational( + PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")), + PolynomialRing({Integer(0), Integer(4)}, SymbolExpression("x")) + ), + Rational( + PolynomialRing({Integer(3)/Integer(2)}, SymbolExpression("x")), + PolynomialRing({Integer(6), Integer(1)/Integer(2), Integer(8), Integer(1)}, SymbolExpression("x")) + ), + Rational( + PolynomialRing({Integer(6), Integer(6), Integer(4)}, SymbolExpression("x")), + PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("x")) + ), + Rational( + PolynomialRing({Integer(7)/Integer(6)}, SymbolExpression("x")), + PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("x")) + ) + }, SymbolExpression("y")) + +local aring = a:getring() -- ZZ +local bring = b:getring() -- QQ +local cring = c:getring() -- ZZ_7 +local dring = d:getring() -- ZZ_14 +local ering = e:getring() -- ZZ[x] +local fring = f:getring() -- QQ[x] +local gring = g:getring() -- ZZ[x][y][z] +local hring = h:getring() -- QQ[z][y][x] +local iring = i:getring() -- ZZ(x) +local jring = j:getring() -- QQ(x) +local kring = k:getring() -- ZZ_5[x] +local lring = l:getring() -- QQ(x)[y] + +starttest("ring construction") +testeq(aring, "ZZ") +testeq(bring, "QQ") +testeq(cring, "Z/Z7") +testeq(dring, "Z/Z14") +testeq(ering, "ZZ[x]") +testeq(fring, "QQ[x]") +testeq(gring, "ZZ[x][y][z]") +testeq(hring, "QQ[z][y][x]") +testeq(iring, "ZZ(x)") +testeq(jring, "QQ(x)") +testeq(kring, "Z/Z5[x]") +testeq(lring, "QQ(x)[y]") +endtest() + +starttest("ring conversion") + +-- Commented-out tests denote elements whos rings are not subrings of the ring that is being converted to + +testringconvert(a, aring, "12", "ZZ") +testringconvert(a, bring, "12/1", "QQ") +testringconvert(a, cring, "5", "Z/Z7") +testringconvert(a, dring, "12", "Z/Z14") +testringconvert(a, ering, "12x^0", "ZZ[x]") +testringconvert(a, fring, "12/1x^0", "QQ[x]") +testringconvert(a, gring, "((12x^0)y^0)z^0", "ZZ[x][y][z]") +testringconvert(a, hring, "((12/1z^0)y^0)x^0", "QQ[z][y][x]") +testringconvert(a, iring, "(12x^0)/(1x^0)", "ZZ(x)") +testringconvert(a, jring, "(12x^0)/(1x^0)", "ZZ(x)") +testringconvert(a, kring, "2x^0", "Z/Z5[x]") +testringconvert(a, lring, "((12x^0)/(1x^0))y^0", "ZZ(x)[y]") + +-- testringconvert(b, aring, "3/2", "ZZ") +testringconvert(b, bring, "3/2", "QQ") +-- testringconvert(b, cring, "3/2", "Z/Z7") +-- testringconvert(b, dring, "3/2", "Z/Z14") +-- testringconvert(b, ering, "3/2", "ZZ[x]") +testringconvert(b, fring, "3/2x^0", "QQ[x]") +-- testringconvert(b, gring, "3/2", "ZZ[x][y][z]") +testringconvert(b, hring, "((3/2z^0)y^0)x^0", "QQ[z][y][x]") +-- testringconvert(b, iring, "3/2", "ZZ(x)") +testringconvert(b, jring, "(3/2x^0)/(1x^0)", "QQ(x)") +-- testringconvert(b, kring, "3/2", "Z/Z5[x]") +testringconvert(b, lring, "((3/2x^0)/(1x^0))y^0", "QQ(x)[y]") + +testringconvert(c, aring, "4", "ZZ") +-- testringconvert(c, bring, "4", "QQ") +testringconvert(c, cring, "4", "Z/Z7") +testringconvert(c, dring, "4", "Z/Z14") +testringconvert(c, ering, "4x^0", "ZZ[x]") +-- testringconvert(c, fring, "4x", "QQ[x]") +testringconvert(c, gring, "((4x^0)y^0)z^0", "ZZ[x][y][z]") +-- testringconvert(c, hring, "4", "QQ[z][y][x]") +testringconvert(c, iring, "(4x^0)/(1x^0)", "ZZ(x)") +-- testringconvert(c, jring, "12/1x^0/1/1x^0", "QQ(x)") +testringconvert(c, kring, "4x^0", "Z/Z5[x]") +-- testringconvert(c, lring, "4", "QQ(x)[y]") + +testringconvert(d, aring, "8", "ZZ") +-- testringconvert(d, bring, "8", "QQ") +testringconvert(d, cring, "1", "Z/Z7") +testringconvert(d, dring, "8", "Z/Z14") +testringconvert(d, ering, "8x^0", "ZZ[x]") +-- testringconvert(d, fring, "8", "QQ[x]") +testringconvert(d, gring, "((8x^0)y^0)z^0", "ZZ[x][y][z]") +-- testringconvert(d, hring, "8", "QQ[z][y][x]") +testringconvert(d, iring, "(8x^0)/(1x^0)", "ZZ(x)") +-- testringconvert(d, jring, "8", "QQ(x)") +testringconvert(d, kring, "3x^0", "Z/Z5[x]") +-- testringconvert(d, lring, "8", "QQ(x)[y]") + +-- testringconvert(e, aring, "3x^2+0x^1+6x^0", "ZZ") +-- testringconvert(e, bring, "3x^2+0x^1+6x^0", "QQ") +-- testringconvert(e, cring, "3x^2+0x^1+6x^0", "Z/Z7") +-- testringconvert(e, dring, "3x^2+0x^1+6x^0", "Z/Z14") +testringconvert(e, ering, "3x^2+0x^1+6x^0", "ZZ[x]") +testringconvert(e, fring, "3/1x^2+0/1x^1+6/1x^0", "QQ[x]") +testringconvert(e, gring, "((3x^2+0x^1+6x^0)y^0)z^0", "ZZ[x][y][z]") +testringconvert(e, hring, "((3/1z^0)y^0)x^2+((0/1z^0)y^0)x^1+((6/1z^0)y^0)x^0", "QQ[z][y][x]") +testringconvert(e, iring, "(3x^2+0x^1+6x^0)/(1x^0)", "ZZ(x)") +testringconvert(e, jring, "(3x^2+0x^1+6x^0)/(1x^0)", "ZZ(x)") +testringconvert(e, kring, "3x^2+0x^1+1x^0", "Z/Z5[x]") +testringconvert(e, lring, "((3x^2+0x^1+6x^0)/(1x^0))y^0", "ZZ(x)[y]") + +-- testringconvert(f, aring, "12x^1+4/5x^0", "ZZ") +-- testringconvert(f, bring, "12x^1+4/5x^0", "QQ") +-- testringconvert(f, cring, "12x^1+4/5x^0", "Z/Z7") +-- testringconvert(f, dring, "12x^1+4/5x^0", "Z/Z14") +-- testringconvert(f, ering, "12x^1+4/5x^0", "ZZ[x]") +testringconvert(f, fring, "12x^1+4/5x^0", "QQ[x]") +-- testringconvert(f, gring, "12x^1+4/5x^0", "ZZ[x][y][z]") +testringconvert(f, hring, "((12/1z^0)y^0)x^1+((4/5z^0)y^0)x^0", "QQ[z][y][x]") +-- testringconvert(f, iring, "12x^1+4/5x^0", "ZZ(x)") +testringconvert(f, jring, "(12x^1+4/5x^0)/(1x^0)", "QQ(x)") +-- testringconvert(f, kring, "12x^1+4/5x^0", "Z/Z5[x]") +testringconvert(f, lring, "((12x^1+4/5x^0)/(1x^0))y^0", "QQ(x)[y]") + +-- testringconvert(g, aring, "", "ZZ") +-- testringconvert(g, bring, "", "QQ") +-- testringconvert(g, cring, "", "Z/Z7") +-- testringconvert(g, dring, "", "Z/Z14") +-- testringconvert(g, ering, "", "ZZ[x]") +-- testringconvert(g, fring, "", "QQ[x]") +testringconvert(g, gring, "((1x^0)y^0)z^3+((1x^0)y^3+(4x^1+-16x^0)y^2+(1x^2+8x^1+2x^0)y^1+(0x^0)y^0)z^2+((-1x^0)y^1+(5x^3+6x^2+7x^1+8x^0)y^0)z^1+((8x^0)y^2+(0x^0)y^1+(1x^2+12x^1+-4x^0)y^0)z^0", "ZZ[x][y][z]") +-- testringconvert(g, hring, "", "QQ[z][y][x]") +-- testringconvert(g, iring, "", "ZZ(x)") +-- testringconvert(g, jring, "", "QQ(x)") +-- testringconvert(g, kring, "", "Z/Z5[x]") +-- testringconvert(g, lring, "", "QQ(x)[y]") + +-- testringconvert(h, aring, "", "ZZ") +-- testringconvert(h, bring, "", "QQ") +-- testringconvert(h, cring, "", "Z/Z7") +-- testringconvert(h, dring, "", "Z/Z14") +-- testringconvert(h, ering, "", "ZZ[x]") +-- testringconvert(h, fring, "", "QQ[x]") +-- testringconvert(h, gring, "", "ZZ[x][y][z]") +testringconvert(h, hring, "((1z^0)y^0)x^3+((1z^0)y^3+(1/9z^1+-16z^0)y^2+(1z^2+8z^1+2z^0)y^1+(0z^0)y^0)x^2+((-1z^0)y^1+(5z^3+6z^2+7z^1+8z^0)y^0)x^1+((8z^0)y^2+(0z^0)y^1+(1z^2+4/5z^1+-4z^0)y^0)x^0", "QQ[z][y][x]") +-- testringconvert(h, iring, "", "ZZ(x)") +-- testringconvert(h, jring, "", "QQ(x)") +-- testringconvert(h, kring, "", "Z/Z5[x]") +-- testringconvert(h, lring, "", "QQ(x)[y]") + +-- testringconvert(i, aring, "", "ZZ") +-- testringconvert(i, bring, "", "QQ") +-- testringconvert(i, cring, "", "Z/Z7") +-- testringconvert(i, dring, "", "Z/Z14") +-- testringconvert(i, ering, "", "ZZ[x]") +-- testringconvert(i, fring, "", "QQ[x]") +-- testringconvert(i, gring, "", "ZZ[x][y][z]") +-- testringconvert(i, hring, "", "QQ[z][y][x]") +testringconvert(i, iring, "(1x^1+-2x^0)/(1x^2+3x^1+3x^0)", "ZZ(x)") +testringconvert(i, jring, "(1x^1+-2x^0)/(1x^2+3x^1+3x^0)", "ZZ(x)") +-- testringconvert(i, kring, "", "Z/Z5[x]") +testringconvert(i, lring, "((1x^1+-2x^0)/(1x^2+3x^1+3x^0))y^0", "ZZ(x)[y]") + +-- testringconvert(j, aring, "", "ZZ") +-- testringconvert(j, bring, "", "QQ") +-- testringconvert(j, cring, "", "Z/Z7") +-- testringconvert(j, dring, "", "Z/Z14") +-- testringconvert(j, ering, "", "ZZ[x]") +-- testringconvert(j, fring, "", "QQ[x]") +-- testringconvert(j, gring, "", "ZZ[x][y][z]") +-- testringconvert(j, hring, "", "QQ[z][y][x]") +-- testringconvert(j, iring, "", "ZZ(x)") +testringconvert(j, jring, "(1x^1+-2x^0)/(1x^2+3/8x^1+3/4x^0)", "QQ(x)") +-- testringconvert(j, kring, "", "Z/Z5[x]") +testringconvert(j, lring, "((1x^1+-2x^0)/(1x^2+3/8x^1+3/4x^0))y^0", "QQ(x)[y]") + +-- testringconvert(k, aring, "", "ZZ") +-- testringconvert(k, bring, "", "QQ") +-- testringconvert(k, cring, "", "Z/Z7") +-- testringconvert(k, dring, "", "Z/Z14") +testringconvert(k, ering, "1x^3+3x^2+1x^1+0x^0", "ZZ[x]") +-- testringconvert(k, fring, "", "QQ[x]") +testringconvert(k, gring, "((1x^3+3x^2+1x^1+0x^0)y^0)z^0", "ZZ[x][y][z]") +-- testringconvert(k, hring, "", "QQ[z][y][x]") +testringconvert(k, iring, "(1x^3+3x^2+1x^1+0x^0)/(1x^0)", "ZZ(x)") +-- testringconvert(k, jring, "", "QQ(x)") +testringconvert(k, kring, "1x^3+3x^2+1x^1+0x^0", "Z/Z5[x]") +-- testringconvert(k, lring, "", "QQ(x)[y]") + +-- testringconvert(l, aring, "", "ZZ") +-- testringconvert(l, bring, "", "QQ") +-- testringconvert(l, cring, "", "Z/Z7") +-- testringconvert(l, dring, "", "Z/Z14") +-- testringconvert(l, ering, "", "ZZ[x]") +-- testringconvert(l, fring, "", "QQ[x]") +-- testringconvert(l, gring, "", "ZZ[x][y][z]") +-- testringconvert(l, hring, "", "QQ[z][y][x]") +-- testringconvert(l, iring, "", "ZZ(x)") +-- testringconvert(l, jring, "", "QQ(x)") +-- testringconvert(j, kring, "", "Z/Z5[x]") +testringconvert(l, lring, "((7/6x^0)/(1x^1+2x^0))y^3+((4x^2+6x^1+6x^0)/(1x^1+3x^0))y^2+((3/2x^0)/(1x^3+8x^2+1/2x^1+6x^0))y^1+((1/4x^1+1x^0)/(1x^1+0x^0))y^0", "QQ(x)[y]") + + + + +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua b/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua new file mode 100644 index 0000000000..1bd315595a --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua @@ -0,0 +1,20 @@ +local a = IntegerModN(Integer(5), Integer(3)) +local b = IntegerModN(Integer(1), Integer(3)) +local c = IntegerModN(Integer(-12), Integer(3)) +local f = IntegerModN(Integer(100), Integer(62501)) +local d = IntegerModN(Integer(16), Integer(36)) +local e = IntegerModN(Integer(27), Integer(36)) + +starttest("modular arithmetic") +testeq(a, "2") +testeq(b, "1") +testeq(c, "0") +testeq(a + b, "0") +testeq(a - b, "1") +testeq(a * b, "2") +testeq(a:inv(), "2") +testeq(b:inv(), "1") +testeq(f:inv(), "61876") +testeq(d * e, "0") +testeq(a * d, "2") +endtest()
\ No newline at end of file diff --git a/macros/luatex/latex/luacas/tex/test/rings/number.lua b/macros/luatex/latex/luacas/tex/test/rings/number.lua new file mode 100644 index 0000000000..9afea0d364 --- /dev/null +++ b/macros/luatex/latex/luacas/tex/test/rings/number.lua @@ -0,0 +1,118 @@ +local a = Integer(5) +local b = Integer(3) +local c = Integer(-12) +local d = Integer("-54321") +local e = Integer("99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999999999999999999999999989999999998") +local f = Integer("-1267650600228229401496703205376") +local g = Integer(16) +local h = Integer(8) +local x = Integer(8) / Integer(5) +local y = Integer(1) / Integer(12) +local z = Integer(-7) / Integer(10) + + +starttest("integer construction") +testeq(a, 5) +testeq(b, 3) +testeq(c, -12) +testeq(d, "-54321") +testeq(e, "99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999999999999999999999999989999999998") +testeq(f, "-1267650600228229401496703205376") +endtest() + +starttest("integer operations") +testeq(-c, 12) +testeq(a + b, 8) +testeq(b - c, 15) +testeq(d - d, 0) +testeq(e + f, "99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999998732349399771770598493296794622") +testeq(a * c, -60) +testeq(f * f, "1606938044258990275541962092341162602522202993782792835301376") +testeq(e * f, "-126752383516820657842978847503263945985032967946239999999987323493997717705985032967944972349399771770598503296781947493995182404784576509143246593589248") +testeq(a // b, 1) +testeq(a % b, 2) +testeq(f // d, "23336289836862896513258283") +testeq(f % d, "-14533") +testeq(e // -f, "78878201913048970415230130190415677050906625793723347950240237316957169209243093407705758276") +testeq(e % -f, "1011644662020502370048160308222") +testeq(c ^ a, -248832) +testeq(d ^ a, "-472975648731213834575601") +testeq(a == b, false) +testeq(b < a, true) +testeq(a <= a, true) +testeq(f < d, true) +testeq(e <= f, false) +endtest() + + + +starttest("integer conversions") +testeq(a / b, "5/3") +testeq(g / c, "-4/3") +testeq(c / b, -4) +endtest() + + +starttest("rational operations") +testeq(-x, "-8/5") +testeq(x + y, "101/60") +testeq(z - y, "-47/60") +testeq(x * z, "-28/25") +testeq(x / y, "96/5") +testeq(y<x, true) +testeq(z<z, false) +testeq(z<=z, true) +endtest() + +starttest("combined integer/rational operations") +testeq(a + x, "33/5") +testeq(x + a, "33/5") +testeq(b - y, "35/12") +testeq(y - b, "-35/12") +testeq(c * y, -1) +testeq(y * c, -1) +testeq(a / x, "25/8") +testeq(x / a, "8/25") +testeq(a/h == x, false) +testeq(h/a == x, true) +testeq(y < b , true) +testeq(b < y, false) +endtest() + +local f = Integer(3) +local g = Integer(216) +local h = Integer(945) +local i = Integer("7766999") +local j = Integer(4) +local k = Integer(8) +local m = Integer(16) +local n = Integer(100000000003) +local o = Integer(200250077) + +starttest("Miller-Rabin Primes") +testeq(f:isprime(), true, f) +testeq(g:isprime(), false, g) +testeq(h:isprime(), false, h) +testeq(i:isprime(), false, i) +testeq(n:isprime(), true, n) +testeq(o:isprime(), false, o) +endtest() + + +starttest("Pollard Rho algorithm") +testeq(f:findafactor(), 3, f) +testeq(g:findafactor(), 2, g) +testeq(h:findafactor(), 3, h) +testeq(i:findafactor(), 41, i) +testeq(j:findafactor(), 2, j) +testeq(k:findafactor(), 2, k) +testeq(m:findafactor(), 2, m) +endtest() + +starttest("prime factorization") +testeq(f:primefactorization(), "* (3 ^ 1)", f, true) +testeq(g:primefactorization(), "(2 ^ 3) * (3 ^ 3)", g, true) +testeq(h:primefactorization(), "(3 ^ 3) * (5 ^ 1) * (7 ^ 1)", h, true) +testeq(i:primefactorization(), "(41 ^ 1) * (189439 ^ 1)", i, true) +testeq(o:primefactorization(), "(10007 ^ 1) * (20011 ^ 1)", o, true) +endtest()
\ No newline at end of file |