4

I want to compute the logarithm of a given stored score and output it in another score, for example input@my_objective -> output@my_objective.

Usually scores are treated as integers, so the solution I saw was the integer log. However, I need higher precision for the output, so the output and input are fixed point decimal numbers. In my case, I plan on using the first 3 digits for the decimal part.

Is it possible to implement a command chain or function that applies this particular type of logarithm? It can be of any base, though I imagine base 2 or base 10 would be easiest.


To my knowledge, there is no way to utilize entities directly for an arbitrary precision solution like you can with trigonometry (set yaw of a marker, and get position of where it's looking for sin/cos), so I have been looking into implementation of logarithms in languages like C.

In particular, what came up is utilizing an approximating rational function for values between 0.5 and 4.0, then using the power/product rule for logarithms (log2(2n x) = n + log2x) to extend this solution for the rest of the numbers. This solution requires some implementation of the highest set bit, which is the part I'm struggling with since most solutions rely on bit-shifting which is base 2, where the fixed point numbers we have don't really follow base 2 (rather they follow base 10). Maybe bit shifting in base 10 would solve the problem?

2
  • While this is definitely on-topic here, you may get a better answer by re-casting this as a pure mathematics problem and posting it on Mathematics instead. See also How much scripting support is allowed for Minecraft commands? Commented Aug 8 at 17:01
  • The Padé approximation of the natural logarithm I mentioned actually came from Mathematics, so it was definitely a good place to start. My issue is trying to implement it within the scope of Minecraft (commands). There is also the chance that there is a way to implement this by exploiting some niche in-game mechanic like how entity rotation can be used to implement trigonometry Commented Aug 8 at 18:44

2 Answers 2

2

Since it appears there are no clever ways to hijack some implicit Minecraft mechanic for logarithms, I made an implementation "from scratch".

Below is the working solution. Since recursion requires multiple files, there are three functions to the solution: math/log10.mcfunction, math/impl/hsd.mcfunction, and math/impl/dshift.mcfunction.

math/log10.mcfunction:

# log10
# takes the score of a particular source and applies the base 10 logarithm,
# then stores the result in the provided result in the given target score.
#
# command format:
# /function math:log10 {targets:<selector>, targetObjective:<objective>, source:<selector>, sourceObjective:<objective>}
#
# targets: the selector in which to store the result. Can be multiple targets.
#          score is a fixed point decimal - first three digits are decimals e.g the score 3140 represents 3.14
# targetObjective: the objective in which to write the result. Must be a single valid objective.
# source: the selector from which to pull the score on which the logarithm will be applied. Must be a single target.
#         the score will be treated as a fixed point decimal like the targets e.g the source 3140 represents 3.14
# sourceObjective: the objective from which to read the source score. Must be a single valid objective.

# 0. copy source score into a variable that can be modified - this will be the result.
$scoreboard players operation #log10_result $(targetObjective) = $(source) $(sourceObjective)
# log of negative numbers yields complex or NaNs, so reset the score instead.
$execute if score #log10_result $(targetObjective) matches ..-1 run scoreboard players reset $(targets) $(targetObjective)
$execute if score #log10_result $(targetObjective) matches ..-1 run return fail

# 1. find highest set digit (hsd), then subtract 4 due to fixed point.
$scoreboard players set #log10_hsd $(targetObjective) -4
$function math:impl/hsd {target:"#log10_hsd",targetObjective:"$(targetObjective)",source:"#log10_result",sourceObjective:"$(targetObjective)"}
# the impl of hsd sets #log10_result = 0, so set it back.
$scoreboard players operation #log10_result $(targetObjective) = $(source) $(sourceObjective)

# 2. digit shift the original number by the hsd above (minus 3). This guarantees the number is now in the range [1.,10.]
# copy hsd into another variable since hsd is needed later on
$scoreboard players operation #log10_hsd_mutable $(targetObjective) = #log10_hsd $(targetObjective)
$function math:impl/dshift {target:"#log10_result",targetObjective:"$(targetObjective)",source:"#log10_hsd_mutable",sourceObjective:"$(targetObjective)"}

# divide the number by 3 to place the range at [1/3,10/3], an acceptable range for the Pade approximation
$scoreboard players set #log10_3 $(targetObjective) 3
$scoreboard players operation #log10_result $(targetObjective) /= #log10_3 $(targetObjective)

# 4. get the Pade approximation numerator and denominator with minimal divisions save for the cubic terms since they can overrun the integer limit
$scoreboard players set #log10_1000 $(targetObjective) 1000

# calculate polynomial powers. For the third power, divide by 1000 since it overrides the integer limit, and it's already accurate enough.
$scoreboard players operation #log10_poly_1 $(targetObjective) = #log10_result $(targetObjective)
$scoreboard players operation #log10_poly_1 $(targetObjective) -= #log10_1000 $(targetObjective)
$scoreboard players operation #log10_poly_2 $(targetObjective) = #log10_poly_1 $(targetObjective)
$scoreboard players operation #log10_poly_2 $(targetObjective) *= #log10_poly_1 $(targetObjective)
$scoreboard players operation #log10_poly_3 $(targetObjective) = #log10_poly_2 $(targetObjective)
$scoreboard players operation #log10_poly_3 $(targetObjective) /= #log10_1000 $(targetObjective)
$scoreboard players operation #log10_poly_3 $(targetObjective) *= #log10_poly_1 $(targetObjective)

# use #log10_result as temporary storage for terms and accumulate #log10_num and #log10_den
$scoreboard players set #log10_result $(targetObjective) 11
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_3 $(targetObjective)
$scoreboard players operation #log10_num $(targetObjective) = #log10_result $(targetObjective)
$scoreboard players set #log10_result $(targetObjective) 60
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_2 $(targetObjective)
$scoreboard players operation #log10_num $(targetObjective) += #log10_result $(targetObjective)
$scoreboard players set #log10_result $(targetObjective) 60000
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_1 $(targetObjective)
$scoreboard players operation #log10_num $(targetObjective) += #log10_result $(targetObjective)
$scoreboard players set #log10_result $(targetObjective) 3
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_3 $(targetObjective)
$scoreboard players operation #log10_den $(targetObjective) = #log10_result $(targetObjective)
$scoreboard players set #log10_result $(targetObjective) 36
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_2 $(targetObjective)
$scoreboard players operation #log10_den $(targetObjective) += #log10_result $(targetObjective)
$scoreboard players set #log10_result $(targetObjective) 90000
$scoreboard players operation #log10_result $(targetObjective) *= #log10_poly_1 $(targetObjective)
$scoreboard players operation #log10_den $(targetObjective) += #log10_result $(targetObjective)
$scoreboard players add #log10_den $(targetObjective) 60000000

# 5. final linear operations to get log10:
# - divide numerator and denominator (remember to scale by 1000)
# - divide by ln10 since the Pade approximation is for ln
# - add log10(3) + hsd to account for the division and shifting
$scoreboard players set #log10_1000_ln10 $(targetObjective) 434
# weirdly, to ensure no overflow, I am doing this flipped, and I am using the ln10 to add back the 1000 decimal
$scoreboard players operation #log10_den $(targetObjective) /= #log10_1000_ln10 $(targetObjective)
$scoreboard players operation #log10_result $(targetObjective) = #log10_num $(targetObjective)
$scoreboard players operation #log10_result $(targetObjective) /= #log10_den $(targetObjective)
$scoreboard players add #log10_result $(targetObjective) 477
$scoreboard players operation #log10_hsd $(targetObjective) *= #log10_1000 $(targetObjective)
$scoreboard players operation #log10_result $(targetObjective) += #log10_hsd $(targetObjective)

# 6. finally, copy the result to the targets
$scoreboard players operation $(targets) $(targetObjective) = #log10_result $(targetObjective)

# 7. cleanup
$scoreboard players reset #log10_result $(targetObjective)
$scoreboard players reset #log10_hsd $(targetObjective)
$scoreboard players reset #log10_hsd_mutable $(targetObjective)
$scoreboard players reset #log10_3 $(targetObjective)
$scoreboard players reset #log10_1000 $(targetObjective)
$scoreboard players reset #log10_poly_1 $(targetObjective)
$scoreboard players reset #log10_poly_2 $(targetObjective)
$scoreboard players reset #log10_poly_3 $(targetObjective)
$scoreboard players reset #log10_num $(targetObjective)
$scoreboard players reset #log10_den $(targetObjective)
$scoreboard players reset #log10_1000_ln10 $(targetObjective)

math/impl/hsd.mcfunction:

$scoreboard players set #hsd_10 $(sourceObjective) 10
# body
$execute if score $(source) $(sourceObjective) matches 1.. run scoreboard players add $(target) $(targetObjective) 1
$execute if score $(source) $(sourceObjective) matches 1.. run scoreboard players operation $(source) $(sourceObjective) /= #hsd_10 $(sourceObjective)
# recursive call
$execute if score $(source) $(sourceObjective) matches 1.. run function math:impl/hsd {target:"$(target)",targetObjective:"$(targetObjective)",source:"$(source)",sourceObjective:"$(sourceObjective)"}
# cleanup
$scoreboard players reset #hsd_10 $(sourceObjective)

math/impl/dshift.mcfunction:

$scoreboard players set #dshift_10 $(targetObjective) 10
# body
$execute if score $(source) $(sourceObjective) matches 1.. run scoreboard players operation $(target) $(targetObjective) /= #dshift_10 $(targetObjective)
$execute if score $(source) $(sourceObjective) matches 1.. run scoreboard players remove $(source) $(sourceObjective) 1
$execute if score $(source) $(sourceObjective) matches ..-1 run scoreboard players operation $(target) $(targetObjective) *= #dshift_10 $(targetObjective)
$execute if score $(source) $(sourceObjective) matches ..-1 run scoreboard players add $(source) $(sourceObjective) 1
# recursive call
$execute unless score $(source) $(sourceObjective) matches 0 run function math:impl/dshift {target:"$(target)",targetObjective:"$(targetObjective)",source:"$(source)",sourceObjective:"$(sourceObjective)"}
# cleanup
$scoreboard players reset #dshift_10 $(targetObjective)

I formatted the functions as macros in order to make things similar to how scoreboard operations behave. The range I ended up choosing is [1/3,10/3] to avoid as much precision loss as I could while keeping the Pade approximation effective (see question).

The result function is surprisingly accurate; for instance, it estimates log105.000 = 0.698 which is off by less than the digits it has than the actual number, 0.69897. Without any modifications, it also gives the exact result for powers of 10, so feeding it 1.000 yields 0.000.

I imagine the general ideas given here could be extended to give more decimal digits of accuracy by adding terms to the Pade approximant up to the point the integer limit can no longer be circumvented. I haven't tested the accuracy of my current solution rigorously, but I am happy with what I have.

Finally, to answer my starting question using this function, I simply need to run the function using the command: /function math:log10 {targets:"output",targetObjective:my_objective,source:"input",sourceObjective:my_objective}

1
  • Very nicely done! Commented Aug 15 at 17:43
0

This is too long for a comment, but this isn't a full answer because I don't know enough about datapack math primitives to give you a working solution; however I do know enough math to give you a few pointers


Usually, if I find my programming language doesn't have good enough support for decimals, I do all my math in a higher power of ten such that it gets me enough integers to have the precision I need. for example, if i need 10^-3 precision, I'll do all my math in as if 1000 is 1. This should hold for log10, as comparisons between log10(1000) and log10(2000) work the same as log10(1) and log10(2).


Bit-shifting is just a fancy (or dumbed down, if you ask IC designers) version of multiplying and dividing by 2, which also happens to be the base we're working in. If you just want to "bit shift" in other bases, including 10, that's as simple as dividing or multiplying in that base.
Trivially: 1234/100 = 12.34, that's the same as 0b100101 [/2, >>1] = 0b10010.1

1
  • A good starting point for the most basic math primitives would be scoreboard operations which give very basic arithmetic, including integer division. Minecraft also has some floating point manipulations. for example, rotating an entity and reading the position 1m in front of it implements trigonometric functions with arbitrary precision. Using a base 10 shifting helps, but that leaves a range for the rational function where precision is falling off (1-10). Is there a way to set this range to something like [0.3, 3]? Commented Aug 8 at 18:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.