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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-10 16:55:48 +01:00
1 parent d0a7d6a314
commit 93d81d7aa9
2 files changed
+83 -17

No files matched your search

+72 -16
View File
@@ -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 (
<SettingsPanel title="Azimuthal integration" dirty={dirty}
onUpload={() => 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}>
<Stack spacing={2} direction="row">
<NumberTextField
default={1.0}
start_val={s.q_spacing}
label={"Q spacing"}
min={1e-5}
units={"A-1"}
float={true}
decimals={5}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setS(prev => ({...prev, q_spacing: val}));
setQSpacingError(err);
}}
fullWidth/>
{(binning === "linear_spacing") ?
<NumberTextField
default={1.0}
start_val={s.q_spacing}
label={"Q spacing"}
min={1e-5}
units={"A-1"}
float={true}
decimals={5}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setQSpacing(val);
setS(prev => ({...prev, q_spacing: val}));
setQSpacingError(err);
}}
fullWidth/>
:
<NumberTextField
default={500}
start_val={s.q_bins}
label={"Q bins"}
min={1}
max={65534}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setQBins(val);
setS(prev => ({...prev, q_bins: val}));
setQBinsError(err);
}}
fullWidth/>
}
<NumberTextField
default={0.1}
@@ -137,6 +181,18 @@ function AzIntSettings({s: serverS}: MyProps) {
}}
/>} label="Force CPU calculation in FPGA workflow"/>
</FormControl>
<Typography>Q binning</Typography>
<FormControl component="fieldset">
<RadioGroup
row
value={binning}
onChange={(event: ChangeEvent<HTMLInputElement>) => selectBinning(event.target.value as Binning)}
>
<FormControlLabel value="linear_spacing" control={<Radio/>} label="Linear, by spacing"/>
<FormControlLabel value="linear_bins" control={<Radio/>} label="Linear, by number of bins"/>
<FormControlLabel value="log10" control={<Radio/>} label="Logarithmic (log10 Q)"/>
</RadioGroup>
</FormControl>
<Typography>Azimuthal bins</Typography>
<FormControl component="fieldset">
<RadioGroup
+11 -1
View File
@@ -1,4 +1,4 @@
import {ChangeEvent, memo, ReactNode, useEffect, useId, useState} from 'react';
import {ChangeEvent, memo, ReactNode, useEffect, useId, useRef, useState} from 'react';
import {
InputAdornment, SxProps,
@@ -40,11 +40,20 @@ function NumberTextField({start_val, default: defaultValue, label, callback, cou
const [val, setVal] = useState(0);
// Unique per field, so each label points at its own input
const id = useId();
// The value this field last reported, and the counter it was set up with: start_val coming back as
// the value just typed is the parent echoing it, and must not rewrite the text being typed (which
// would round it to `decimals` or drop what the number does not show, like a trailing zero).
const reported = useRef<number | undefined>(undefined);
const lastCounter = useRef<number | undefined>(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);
};