From 93d81d7aa9aca5c3355cfcbf5bc022c0cd55bfcb Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 10 Nov 2026 16:06:46 +0100 Subject: [PATCH] Frontend: choose the Q binning of azimuthal integration The azimuthal-integration panel offers linear binning by spacing, linear binning by number of bins, and logarithmic (log10 Q) binning by number of bins. A number field no longer rewrites the text being typed when the value it just reported comes back from the parent: a spacing with more decimals than the field displays (0.000875) or written in exponent form (8.7e-4) stays as typed and is sent as typed, instead of the text being rounded to the displayed decimals. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK --- frontend/src/components/AzIntSettings.tsx | 88 +++++++++++++++++---- frontend/src/components/NumberTextField.tsx | 12 ++- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/AzIntSettings.tsx b/frontend/src/components/AzIntSettings.tsx index 73beedd98..20bdfbbf3 100644 --- a/frontend/src/components/AzIntSettings.tsx +++ b/frontend/src/components/AzIntSettings.tsx @@ -2,13 +2,22 @@ import {ChangeEvent, memo, useEffect, useState} from 'react'; import {FormControlLabel, Checkbox, Stack, Radio, RadioGroup, Typography} from "@mui/material"; import NumberTextField from "./NumberTextField"; -import {azim_int_settings} from "../client"; +import {azim_int_settings, q_binning} from "../client"; import {putConfigAzimIntMutation} from "../client/@tanstack/react-query.gen"; import _ from "lodash"; import FormControl from "@mui/material/FormControl"; import SettingsPanel from "./SettingsPanel"; import {useUpload} from "./useUpload"; +// Linear bins are set by their spacing or by their number; log10 bins only by their number. +type Binning = "linear_spacing" | "linear_bins" | "log10"; + +function binningOf(s: azim_int_settings): Binning { + if (s.q_binning === q_binning.LOG10) + return "log10"; + return (s.q_bins !== undefined) ? "linear_bins" : "linear_spacing"; +} + type MyProps = { s?: azim_int_settings }; @@ -19,6 +28,7 @@ const default_azim_int_settings : azim_int_settings = { high_q_recipA: 5, low_q_recipA: 0.1, q_spacing: 0.1, + q_binning: q_binning.LINEAR, azimuthal_bins: 1, force_cpu: false } @@ -30,7 +40,11 @@ function AzIntSettings({s: serverS}: MyProps) { const [lowQError, setLowQError] = useState(false); const [highQError, setHighQError] = useState(false); const [qSpacingError, setQSpacingError] = useState(false); + const [qBinsError, setQBinsError] = useState(false); const [highQ, setHighQ] = useState(5); + // The spacing and bin count last entered, kept while the other way of binning is selected. + const [qSpacing, setQSpacing] = useState(0.1); + const [qBins, setQBins] = useState(500); const { submit, pending, snackbar } = useUpload(putConfigAzimIntMutation()); useEffect(() => { @@ -39,33 +53,63 @@ function AzIntSettings({s: serverS}: MyProps) { setLastDownloadedS(serverS); setDownloadCounter(c => c + 1); setHighQ(serverS.high_q_recipA ?? highQ); + setQSpacing(serverS.q_spacing ?? qSpacing); + setQBins(serverS.q_bins ?? qBins); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [serverS]); const dirty = !_.isEqual(s, lastDownloadedS); + const binning = binningOf(s); + + const selectBinning = (b: Binning) => { + setS(prev => { + const rest = _.omit(prev, ["q_spacing", "q_bins"]); + if (b === "linear_spacing") + return {...rest, q_binning: q_binning.LINEAR, q_spacing: qSpacing}; + return {...rest, q_binning: (b === "log10") ? q_binning.LOG10 : q_binning.LINEAR, q_bins: qBins}; + }); + }; return ( submit(s)} uploadDisabled={pending || ((s.high_q_recipA !== undefined) && (s.high_q_recipA <= s.low_q_recipA)) - || highQError || lowQError || qSpacingError} + || highQError || lowQError + || (binning === "linear_spacing" ? qSpacingError : qBinsError)} snackbar={snackbar}> - { - setS(prev => ({...prev, q_spacing: val})); - setQSpacingError(err); - }} - fullWidth/> + {(binning === "linear_spacing") ? + { + setQSpacing(val); + setS(prev => ({...prev, q_spacing: val})); + setQSpacingError(err); + }} + fullWidth/> + : + { + setQBins(val); + setS(prev => ({...prev, q_bins: val})); + setQBinsError(err); + }} + fullWidth/> + } } label="Force CPU calculation in FPGA workflow"/> + Q binning + + ) => selectBinning(event.target.value as Binning)} + > + } label="Linear, by spacing"/> + } label="Linear, by number of bins"/> + } label="Logarithmic (log10 Q)"/> + + Azimuthal bins (undefined); + const lastCounter = useRef(undefined); // Something has to change in parent to trigger setup => otherwise this makes infinite loop. // In case of error state start_val could stay same, but we want to return to "safe" set // so there is external counter that if changed reset is triggered. useEffect(() => { + const counterChanged = counter !== lastCounter.current; + lastCounter.current = counter; + if (!counterChanged && start_val !== undefined && start_val === reported.current) + return; let v : number = 0; if (start_val !== undefined) v = start_val; @@ -78,6 +87,7 @@ function NumberTextField({start_val, default: defaultValue, label, callback, cou setErr(new_err); setVal(new_val); setText(new_text); + reported.current = new_val; callback(new_val, new_err); };