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:
1 parent
d0a7d6a314
commit
93d81d7aa9
2 files changed
+83
-17
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user