What's the correct way to calculate centipawn loss? I've been letting the engine analyze a position at depth D, then making the player's move and letting the engine analyze the resulting position at the same depth D, and then taking the difference between the first score and the second score (both from the same player's perspective, of course). This procedure has the property that it's possible to have a negative centipawn loss: it will be negative if the engine prefers the player's move to its initial choice after seeing one ply farther in the line beginning with the player's move. Is this expected and acceptable? The alternative would be to have the engine analyze the position after the player's move at depth D - 1, which would guarantee a non-negative centipawn loss.
No. Your assumption is based on searching everything up to depth D, but no modern chess engine would do that. It's complicated to explain, but fair to assume only some subtrees up to depth D are explored. The other trees got "cut" off for various reasons, most likely they are not good.

Chess programming is all about skipping the search tree accurately. The conditions could depend on the depth to the root, previously visited nodes, and many others. As you make a move on the board, all parameters change, even the cache for keeping the transposition table change. Trees that are previously visited may now be skipped, and vice versa. The change of information in the transposition table could have a significant impact in the new search path.


  it will be negative if the engine prefers the player's move to its initial choice after seeing one ply farther in the line beginning with the player's move. Is this expected and acceptable? 


Anything can happen. The difference can be positive, negative, or remain unchanged.