Skip to content

Tomographic 2pcf calculation with treecorr#297

Open
LisaGoh wants to merge 3 commits into
feature/sp_validation-extend-to-tomographyfrom
296-tomographic-2pcf-calculation-with-treecorr
Open

Tomographic 2pcf calculation with treecorr#297
LisaGoh wants to merge 3 commits into
feature/sp_validation-extend-to-tomographyfrom
296-tomographic-2pcf-calculation-with-treecorr

Conversation

@LisaGoh

@LisaGoh LisaGoh commented Jul 22, 2026

Copy link
Copy Markdown
Member

This PR resolves #296. I've added functionalities in calculate_2pcf() in real_space.py to calculate the 2PCFs given two tomographic bin IDs as input. Ditto for xip_xim.py. The functions default to the non-tomographic case if the bins are not defined.

I've commented out all the functions that do the writing out of the 2PCFs, in preparation for the sacc_io output format instead.

@LisaGoh LisaGoh self-assigned this Jul 22, 2026
@LisaGoh
LisaGoh requested review from cailmdaley and sachaguer July 22, 2026 15:57
@LisaGoh LisaGoh linked an issue Jul 22, 2026 that may be closed by this pull request
@sachaguer

sachaguer commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Great to see this PR Lisa. Something I was considering doing after the harmonic space and rho-/tau-statistics so thanks a lot for taking this task over. I mostly have global implementation design comment that we should agree on and that mostly come from my work on harmonic space (#221) and rho-/tau-statistics (#295).

Global design comment:

  • In your PR you rely on an external script to perform your computations and your modifications to real_space.py add a new argument to specify which tomographic bin to use. Alternatively, for the harmonic space and the rho-/tau- the functions calculate_<what_you_calculate> iterate upon all the versions given as input to the CosmologyValidation object. I added an extra argument tomography being a boolean to now whether to run with or without tomography. It's particularly true for the harmonic space but I decomposed the functions in utility/helper functions and the calculate functions that iterate over all the versions. I personally think the latter design is the best for what we want to do and reading your PR, the calculate_2pcf function you improved on reads to me like such a helper function. I would suggest trying to avoid relying on an external script to iterate upon the versions and tomographic bins and have it embedded within CosmologyValidation. It's a big comment that implies some work, I am happy to commit to the branch if you agree this vision should be our target and want help throughout the implementation.

I will comment directly in the code for implementation related comments.

@sachaguer sachaguer closed this Jul 23, 2026
@sachaguer sachaguer reopened this Jul 23, 2026
"var_method": "jackknife" if int(npatch) > 1 else "shot",
}

pol_factor = self.pol_factor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The polarisation factor is necessary to match NaMaster convention. I am not sure this is necessary for real space. Have you check it? I think the previous version was not using it.
In addition, if we need to flip sign, I think treecorr has built-in arguments for that we should probably use rather than multiplying manually.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, ok I had thought it was included to add flexibility to account for the case where e2 flips sign. If we agree upstream that e2 doesn't require a sign flip thenI can remove it!

out_fname = self._output_path(
f"{ver}_xi_minsep={treecorr_config['min_sep']}_maxsep={treecorr_config['max_sep']}_nbins={treecorr_config['nbins']}_npatch={npatch}.txt"
)
if tomo_bin1 is None and tomo_bin2 is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the global design logic, we can avoid having a if ... else statement for the tomography with some duplication of code. See e.g. here for an implementation design I adopted. We should homogeneise either to this choice or another one.

# LG TO-DO: No longer writing out text file, change to sacc_io method
# gg.write(out_fname, write_patch_results=True, write_cov=True)

if not os.path.exists(patch_file_bin1) or not os.path.exists(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You left a comment about this elsewhere. I think we can be smart about the patches and use the same across all the tomographic bins. I don't see a reason why we would need different patches for the different tomo bins. From the first produced catalog it must be possible to extract the patch_centers and use it as input to any other treecorr.Catalog produced afterwards; hence the need to have a function that computes all the tomographic bins at once.

# Add correlation object to class
if not hasattr(self, "cat_ggs"):
self.cat_ggs = {}
self.cat_ggs[ver] = gg

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The saving dictionnary should now also specify the bins for the tomographic case.

else:
print(f"Writing Map2 to output file {out_fname_map2} ")
gg.writeMapSq(out_fname_map2, R=theta_map, m2_uform="Schneider")
self._map2[ver][f"{tomo_bin1}_{tomo_bin2}"] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to homogeneise with harmonic space (#221) and rho-/tau-stats (#295)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll follow the convention of f"tomo_bin_{bin_key1}_tomo_bin_{bin_key2}"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's fine for everyone. I am happy to take any suggestion

@LisaGoh

LisaGoh commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Thank you for your comments! Yes I see what you mean, I was contemplating between doing it this way (having the tomographic bins as input) vs giving it a compute_tomography as a boolean, then computing all the tomographic 2PCFs at one go.

One reason why I chose to do it this way was that I saw other scripts (such as pure_eb.py) require the specific singular gg correlation as output, which then gets fed into other libraries, such as cosmo_numba for example in b_modes.py. I didn't want to mess up the dependencies on the other scripts, so I figured this was the cleanest way to do it for now. But if the ultimate goal is to calculate all the tomographic 2PCF's in one go in calculate_2pcf(), I'd be happy to update it, and adapt all the othe dependent scripts to account for a tomographic gg output as well!

@sachaguer

Copy link
Copy Markdown
Contributor

I think that what does not undermine other functions is to keep this function as a child function called by parents functions from pure_eb.py and real_space.py and rename to have a consistent convention across.

@cailmdaley cailmdaley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core catalogue-splitting logic in calculate_2pcf looks right, and thanks for keeping the non-tomo default path intact. Most of my comments are about the interim I/O state pending sacc_io — all fine to defer, with one guard-rail — plus two small real bugs and a couple of convention alignments with #295/#298. And +1 to the design you and Sacha converged on (gating on a compute_tomography boolean inside CosmologyValidation); happy for the plotting/wiring bits to land in that future PR.

— Claude, on behalf of Cail

print(f"Evaluating {len(generated_cuts)} scale cut combinations")

# Call b_modes function with scale cuts list
results = calculate_cosebis(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rebinding clobbers the accumulator dict initialized above (results = {bin_key: None …}), and then line 161's results[bin_key] = results self-references — even the plain non-tomo single-scale-cut case returns a mangled dict. A differently-named inner variable (res = calculate_cosebis(...); results[bin_key] = res) should fix it — same in the else branch below.

len(self.results[ver].dat_shear), dtype=bool
)
else:
mask_bin1 = self.results[ver].dat_shear["tom_bin_id"] == bin1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things in one: this literal is missing an 'o' (your xip_xim.py default is tomo_bin_id), and #295/#298 both resolve the column via cc[ver]["shear"]["tomo_bin_col"] — resolving through the config here too (and in calculate_aperture_mass_dispersion) keeps the three PRs on one convention.

ggs = {f"tomo_bin_{b1}_tomo_bin_{b2}": None for b1, b2 in tomo_bin_pairs}

# LG TO-DO: Change to sacc_io method
out_fname = self._output_path(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the writes are deferred to sacc_io, suggest also disabling this os.path.exists read branch until then: the filename has no bin pair in it, so a stale file from an old run would be read into every bin pair — silently identical data across all auto/cross combinations. (When sacc_io lands, encoding the pair à la #295's basename() solves it.) Same applies to the aperture-mass cache below, where the read branch additionally stores under a different key format/value type than the compute and plot code expect.

if compute_tomography:
tomo_bin_ids, tomo_bin_pairs = self._get_tomo_bins(ver)
self.print_magenta(
f"Computing tomographic ξ± for {ver} with {len(tomo_bin_pairs)} bins."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny: if a version lacks tomography info, _get_tomo_bins returns (None, None), so this len() raises TypeError before the intended, informative ValueError — moving the guard above the print fixes it (same in calculate_aperture_mass_dispersion).

cov_path=cov_path,
scale_cuts=scale_cuts,
evaluate_all_scale_cuts=evaluate_all_scale_cuts,
compute_tomography=False, # LG: Hardcoded to False for now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine to leave for the future PR — just noting plot_cosebis also still assumes the old un-binned return shape of calculate_cosebis (results are now one level deeper, even non-tomographically), so COSEBIs plotting is broken until that pass; worth a TODO so it's not a surprise.

@sachaguer

sachaguer commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@LisaGoh just a note to point you to this function I coded. It allows to plot xi_+/xi_- like plot as long as you provide a function that says what to plot in each tomo bin axes. I was thiking this could be use for any plot involving 2pcfs of any sort so don't hesitate to use it/update it if it's useful and avoids code duplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tomographic 2PCF calculation with TreeCorr

3 participants