-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/new build simulators #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/controller-updates
Are you sure you want to change the base?
Feature/new build simulators #489
Conversation
…n conversion complete for the front end
…arium/simularium-viewer into feature/base-remote-client
…arium/simularium-viewer into feature/base-remote-client
…arium/simularium-viewer into feature/base-remote-client
…arium/simularium-viewer into feature/base-remote-client
…arium/simularium-viewer into feature/new-build-simulators
| ///// File Changing ///// | ||
|
|
||
| private clearFileResources(reuseSimulator = false): void { | ||
| this.closeConversionConnection(); | ||
| this.stop(); | ||
| this.isFileChanging = false; | ||
| if (!reuseSimulator) { | ||
| this.simulator = undefined; | ||
| } | ||
| this.playBackFile = ""; | ||
| this.visData.clearForNewTrajectory(); | ||
| this.simulator?.abort(); | ||
| this.pause(); | ||
| if (this.visGeometry) { | ||
| this.visGeometry.clearForNewTrajectory(); | ||
| this.visGeometry.resetCamera(); | ||
| } | ||
| } | ||
|
|
||
| public handleFileChange( | ||
| simulariumFile: ISimulariumFile, | ||
| fileName: string, | ||
| geoAssets?: { [key: string]: string } | ||
| ): Promise<FileReturn> { | ||
| if (!fileName.includes(".simularium")) { | ||
| throw new Error("File must be a .simularium file"); | ||
| } | ||
|
|
||
| if (geoAssets) { | ||
| return this.changeFile({ simulariumFile, geoAssets }, fileName); | ||
| } else { | ||
| return this.changeFile({ simulariumFile }, fileName); | ||
| } | ||
| } | ||
|
|
||
| public cancelCurrentFile(newFileName: string): void { | ||
| this.isFileChanging = true; | ||
| this.playBackFile = newFileName; | ||
|
|
||
| // calls simulator.abort() | ||
| this.stop(); | ||
|
|
||
| this.visData.WaitForFrame(0); | ||
| this.visData.clearForNewTrajectory(); | ||
| public clearFile(): void { | ||
| this.isFileChanging = false; | ||
| this.clearFileResources(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clearFile as a part of the public API is not functionally changed, but the business logic has been moved into clearFileResources to allow some internal flexibility when we want to switch between remote files and reuse the simulator.
src/Simulator/ISimulator.ts
Outdated
| export const getSimulatorClassFromParams = (params?: SimulatorParams) => { | ||
| if (!params || !params.fileName) { | ||
| return { simulatorClass: null, typedParams: null }; | ||
| } | ||
| if ("netConnectionSettings" in params) { | ||
| return { | ||
| simulatorClass: RemoteSimulator, | ||
| typedParams: params as RemoteSimulatorParams, | ||
| }; | ||
| } else if ("clientSimulatorImpl" in params) { | ||
| return { | ||
| simulatorClass: ClientSimulator, | ||
| typedParams: params as ClientSimulatorParams, | ||
| }; | ||
| } else if ("simulariumFile" in params) { | ||
| return { | ||
| simulatorClass: LocalFileSimulator, | ||
| typedParams: params as LocalFileSimulatorParams, | ||
| }; | ||
| } else { | ||
| return { simulatorClass: null, typedParams: null }; | ||
| } | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSimulatorClassFromParams handles generating the right type of simulator for the incoming params, the simulators have runtime checks to make sure they have received the right params as this can be run in JS on the client side. @meganrm outlined this function in comments on previous PR
| const mockConversionClient = { | ||
| setOnConversionCompleteHandler: vi.fn().mockImplementation(() => { | ||
| console.log("mocked handler"); | ||
| }), | ||
| cancelConversion: vi.fn(), | ||
| disconnect: vi.fn(), | ||
| convertTrajectory: vi.fn().mockResolvedValue(undefined), | ||
| sendSmoldynData: vi.fn().mockResolvedValue(undefined), | ||
| lastRequestedFile: "", | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't mock the internal functionality of the conversion client or the messages we expect to receive, but rather the presence of the functions and if they get called at the right time. Internal testing out of scope imo.
| vi.mock("../Simulator/ISimulator", async () => { | ||
| const actual = await vi.importActual< | ||
| typeof import("../Simulator/ISimulator") | ||
| >("../Simulator/ISimulator"); | ||
|
|
||
| return { | ||
| ...actual, | ||
| getSimulatorClassFromParams: (params?: SimulatorParams) => { | ||
| const result = actual.getSimulatorClassFromParams(params); | ||
| if (result.simulatorClass === RemoteSimulator) { | ||
| return { ...result, simulatorClass: DummyRemoteSimulator }; | ||
| } | ||
| return result; | ||
| }, | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may seem a bit convoluted and I'm open to suggestions, but what we are achieving here is getSimulatorClassFromParams returns a dummy remote simulator.
A reason to do this is that by removing the old controller params, we lost the code path to let the client provide the simulator they want to use, and so we need a way to get the dummy in play for testing. This is bit of a hack, but keeps the mess here in the testing and out of the production code.
src/Simulator/ISimulator.ts
Outdated
| } else { | ||
| return { simulatorClass: null, typedParams: null }; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to be the wrong place to put this code. ISimulator should not depend on the files that implement ISimulator.
Also do you think this function could probably be more like a factory method that actually returns a ISimulator instance (call new ClientSimulator, or new RemoteSimulator etc, inside the if statements)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@meganrm want to weigh in on this? I left this where and as you wrote it for the most part.
src/controller/index.ts
Outdated
| import { ISimulator } from "../simularium/ISimulator.js"; | ||
| import { LocalFileSimulator } from "../simularium/LocalFileSimulator.js"; | ||
| import { | ||
| getSimulatorClassFromParams, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prob could just define getSimulatorClassFromParams in here, or make a new ts module called SimulatorFactory.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into SimulatorFactory.ts
src/controller/index.ts
Outdated
| "netConnectionSettings" in params && | ||
| this.simulator instanceof RemoteSimulator && | ||
| this.simulator.isConnectedToRemoteServer() && | ||
| this.lastNetConnectionConfig === params.netConnectionSettings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this function, but does this need to be a deeper equality check to compare the netConnectionSettings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to ==
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that (changing from === to ==) would be shallower... deeper check would be to compare the equality of the actual properties of the object. (technically double equals is not "shallower" but is less strict)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, I misunderstood your comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed a more robust check
src/Simulator/types.ts
Outdated
| prefetchFrames?: boolean; | ||
| } | ||
|
|
||
| export interface ClientSimulatorParams extends BaseSimulatorParams { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious, why do these all extend BaseSimulatorParams? it feels like BaseSimulatorParams is never going to grow beyond fileName and ClientSimulatorParams doesn't even have a fileName..
Is BaseSimulatorParams used anywhere outside of this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm misunderstanding you, but ClientSimulatorParams does have a "file name" in a sense. If you look at the way the test bed loads client simulators we use strings in the ID field to serve as file name, and the downstream simulator factory method and initialize calls expect all incoming params to have a value for that field.
It's true that you currently can't actually load a client simulator file from your local machine (like via drag and drop, although that feature should exist imho) but even the "file name" for a remote trajectory is more of an ID. I think we could have something saved on octopus with a random string and as long as the server sent the right data back it's just a convention that the remote files have a .simularium extension.
I'd have to poke around but it seems like a pretty baked-in convention/assumption that everything that gets loaded in the viewer has a file name, would take some refactoring in a few places to take it out.
From test bed configs:
{
id: "blood-plasma-1.0.simularium",
name: "Blood Plasma",
simulatorType: SimulatorModes.remotePrecomputed,
},
{
id: "TEST_POINTS",
name: "Point Simulation",
simulatorType: SimulatorModes.localClientSimulator,
},
```
As for the base class we could just make each param interface have fileName rather than use a base class though, I don't remember why we did that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe trajectoryId or simulationId would be more general than filename then. I think my comment was partly motivated because it seemed odd to introduce a base class for just one single variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, pushed a quick change removing the base class but retaining fileName.
If you'd like to also see the move towards a different naming I'll work on that as well, but that naming convention is a bit more spread out so might be more involved.
Co-authored-by: toloudis <[email protected]>
Co-authored-by: toloudis <[email protected]>
…ularium/simularium-viewer into feature/new-build-simulators
| public clearPendingFile() { | ||
| this.setState({ filePending: null }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quick sanity check: this looks like something important, where the code wanted to know that there was a file in-flight? is it really ok to remove it and within scope of this PR? or is this dead code that was never used?
toloudis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is looking good. Only concern now, is there more work after merging this to update simularium-website? 😢 And mucho testing with existing production simulations from the server!
| LocalFileSimulatorParams, | ||
| } from "./types"; | ||
|
|
||
| export const getSimulatorClassFromParams = (params?: SimulatorParams) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: just for the record, a true "factory" would actually CREATE and return instances of these classes. Just for clarity of terminology, as this is sort of a meta-factory which is just finding out what kind of class to create.
| } | ||
|
|
||
| export interface ClientSimulatorParams { | ||
| fileName: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my comments around this code earlier were based on thinking that for a ClientSimulator, this string is not really a file name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there already a PR in simularium-website that accounts for the interface changes in here?
| // export interface NetConnectionParams { | ||
| // serverIp?: string; | ||
| // serverPort?: number; | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can delete now!
| newConfig?.serverIp != null && | ||
| newConfig?.serverPort != null && | ||
| lastConfig.serverIp === newConfig.serverIp && | ||
| lastConfig.serverPort === newConfig.serverPort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you used lastConfig?.serverPort a couple lines up, why not use ?. here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we make this shouldReuseSimulator testable to demonstrate it returns true or false for the conditions we care about?... (edit) I found the tests but they are higher level. would be cool if we could actually unit test just this function but i am basically satisfied for this.
|
I have read through this and looks good, but I still want to test it with the education app. Could be of interest: One thing I've noticed using the main branch is the "wait for frame" is never really used because the cache is cleared so often the waited for frame gets reset before the frames arrive |
I'll check that out. It's all getting a little foggy, but in some branch related to prefetching I have a callback based version of |
| if ( | ||
| "simulariumFile" in params && | ||
| !params.fileName.includes(".simularium") | ||
| ) { | ||
| this.handleError("File must be a .simularium file"); | ||
| return Promise.reject({ status: FILE_STATUS_FAIL }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to use this for the binding affinity app. For local files, simulariumFile is the implementation, not a filename, so it's not going to include a .simularium extension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed these lines and the file loaded. For context, this is how I call changeFile:
const response = await fetch(url);
if (response.ok) {
const blob = await response.blob();
const simulariumFile = await loadSimulariumFile(blob);
await simulariumController.changeFile({
fileName: "example",
simulariumFile: simulariumFile,
});
const plotData = simulariumFile.getPlotData();
setTrajectoryPlotData(plotData[0].data as ScatterTrace[]); // we're not using histograms
} else {
throw new Error(`Failed to fetch - ${response.status}`);
}
|
if you haven't already, I think you should make a branch on the website repo that installs this version and make sure every way of loading a file works: url, local, drag and drop local, examples etc |
YES. test all the things. |
Time Estimate or Size
medium-large
Problem
Final round of controller refactoring of controller and simulator params.
@meganrm @toloudis The approach to most of these changes was discussed in detail at our meeting a few weeks back. I officially futzed with this for too long and then went on vacation. Simularium is paused, would be good to get this done but I know it's not high priority.
More than half of the lines of changes are new tests or moved utils, so the line count isn't as bad as it seems.
After this I will finalize prefetching and controller speed with these new changes and that will get me close to putting simularium work to bed for now.
The utilities that are shared by the
BinaryFIleReaderandSimulariumControllertests I moved to a util file.Solution
We now have typed interfaces for the argument passed to
changeFile, there is no more code path to initialize the controller with params. I've updated the readme to show thatchangeFileneeds to be called imperatively or in a lifecycle method in order to load a trajectory.I added quite a bit of testing to the controller, where previously there wasn't much. There could be more, but the goal was to add tests relevant to this change set, and establish a base of coverage so that in the future when we add features, adding tests doesn't require as big of alift.
Type of change