Found it documented in Horizon source code:
// Operations within the stellar network have a total order, expressed by three
// pieces of information: the ledger sequence the operation was validated in,
// the order which the operation's containing transaction was applied in
// that ledger, and the index of the operation within that parent transaction.
//
// We express this order by packing those three pieces of information into a
// single signed 64-bit number (we used a signed number for SQL compatibility).
//
// The follow diagram shows this format:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Ledger Sequence Number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Transaction Application Order | Op Index |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
You can find a the Go code in the same link.
Here is a sample Python example:
LEDGER_MASK = (1 << 32) - 1
TRANSACTION_MASK = (1 << 20) - 1
OPERATION_MASK = (1 << 12) - 1
LEDGER_SHIFT = 32
TRANSACTION_SHIFT = 12
OPERATION_SHIFT = 0
def calculate_operation_id(
ledger_sequence: int,
transaction_order_in_ledger: int, # 1-based
operation_order_in_transaction: int, # 1-based
):
operation_id = (ledger_sequence & LEDGER_MASK) << \
LEDGER_SHIFT
operation_id = operation_id | (transaction_order_in_ledger & TRANSACTION_MASK) << \
TRANSACTION_SHIFT
operation_id = operation_id | (operation_order_in_transaction & OPERATION_MASK) << \
OPERATION_SHIFT
return operation_id