We implemented a node to load various kinds of data, notably `.npy`,
`.txt`, `.txt.gz`, and `.csv`. The `DataFileImporterNode` really should
expose some settings for setting name/mathtype/physical type/unit of
each unit, and/or treating a column from 2D data as index coordinates.
But the nuances of doing this in a manner general enough to deal with
!=2D data was a lot, and we needed similar abilities in the general math
system anyway.
So, we delved back into the `FilterMathNode` and a little into the
`TransformMathNode`. Fundamentally, a few difficult operations came out
of this:
- Filter:SliceIdx: Slice an array using the usual syntax, as baked into the
function.
- Filter:PinIdx: Pin an axis by its actual index.
- Filter:SetDim: Set the `InfoFlow` index coordinates of an axis to a specific,
loose-socket provided 1D array, and use a common symbol to set the
name+physical type (and allow specifying an appropriate unit).
- Transform:IntDimToComplex: Fold a last length-2 integer-indexed axis
into a real output type, which removes the dimension and produces a
complex output type. Essentially, this is equivalent to folding it as
a vector and treating the `R^2` numbers as real/imaginary, except this
is more explicit.
By combining all of these, we managed to process and constrain the medium data to
be a well-suited, unit-aware (**though not on the output (yet)**) `wl->C` tensor.
In particular, the slicing is nice for avoiding discontinuities.
Workflow-wise, we'll see how important these are / what else we might
want. Also, it turns out Blender's text editor is really quite nice for
light data-text viewing.
We also implemented a JIT-based `rescale` for `ArrayFlow`, where the
same function as for `LazyArrayRangeFlow` passes through and can do an
arbitrary symbolic shift/rescale/order-preserving whatever.
To make this fast for `ArrayFlow`, we utilize a "test" variable `a`,
put it through the function and rescale/strip its units, then `lambdify`
it and broadcast the function onto `ArrayFlow.values`.
It's an immensely clean way of working.
The `lambdify` seems fast, interestingly, but ideally we would of course
also cache that somehow.
Some details remain:
- Fourier Transform index bounds / length are currently not presumed
known before actually computing them; it's unclear what's best here,
as the design cross-section of physical expectation, mathematical
correctness, and ease of implementation (especially trying to keep
actual data out of the `InfoFlow` hot-path). For now, the `0..\infty`
bounds **will probably break** the `Viz` node.
- We've reverted the notion that particular `sim_symbol`s must have a
unit pre-defined, which causes a little more complexity for nodes like
`TemporalShape`. This question is going to need resolving
- The whole `InfoFlow` object really seems to be at the heart of certain
lagginess when it comes to the math system. It, together with the
index representations, would benefit greatly from a principled
refactor.
- The `Viewer` node is broken for 3D preview; see #70.
Closes#59. Progress on #54.
We reimplemented the OperateMathNode entirely, and it should now be
relatively to entirely robust. It works based on an enum, just like
`MapMathNode`, which enormously simplifies the node itself. Note that
we're not quite done with sympy+jax implementations of everything, nor
validating that all operations have an implementation valid for all the
`InfoFlow`s that prompted it, but, nonetheless, such fixes should be
easy to work out as we go.
We also finally managed to implement "remembering". In essence,
"remembering" causes `FlowPending` to not reset the dynamic enums in
math / extract nodes. The implementation is generally per-node, and a
bit of boilerplate, but it seems quite robust on the whole - allowing
one to "keep a node tree around" even after removing simulation data.
Extract of sim data is of course more leniant, as it also "remembers"
when there is `NoFlow`. Only new `Sim Data` will reset the extraction
filter enum.
Caches are kept (not memory efficient, but convenient for me), but
only `FlowPending` is actually passed along the `InfoFlow`. This is the
semantics we want - having to deal with this all is a tradeoff of
having data-driven enums, but all in all, it still feels like the right
approach.
We also implement `BLField` support for Blender `IDStruct` types.
There's a crash haunting us specifically with the cylinder array. Other
primitives (esp. ring) work just fine, as does previews of nested-linked
node groups. The crash triggers specifically when the file is saved and
reloaded, whereafter all the `load_post` handlers run fine (like -
extremely fine, we can perfectly access and dereference all of the node
groups, seemingly all the objects, etc.). Then, crash.
We're also discovering that `id_properties_ui` is completely useless for
after-the-fact updating of custom properties within sockets. Which is a
great surprise, and I have trouble thinking it's on purpose - the data
is stored somewhere, after all. All the forced updates/redraws/etc. in
the world don't seem to change this.
We may have to go back to the drawing board with dynamically-updated
min/max. The entire infrastructure with `SocketDef` altering sockets
after creation is entirely, _violently_ unsuited to do a static
modification. But the bare fact is, the dynamic modification methods are
falling short. It's kind of important stuff, this stuff.
Re-added the cylinder primitive (it's quite important), including an
appropriate asset `.blend`.
Also finished (I think?) a quick cleanup round from the enhanced
`ManagedBLModifier`, which now owns the mesh and doesn't need a seperate
`ManagedBLMesh`.
This seems faster for some reason, but more importantly, it simplifies a
bunch of the previewing code.
Finally, fixed a small issue with the extract data node where its
`ExprSocket` still had the `FlowKind.Array`, causing it to not be able
to be connected to the whole math system. So, yeah.
We now have a single node for all temporal shapes, which is extremely
usable. Note that we found a bug where input socket caching seems to
survive changes to loose inputs / socket set-driven alterations, which
prevents output from being able to switch with the socket set. We'll
make an issue for it whenever convenient.
Work also continued very briskly with the `SimSymbol` abstraction, which
is really, really working out.
Closes#67.
Driven solely by the Expr socket, we've completely replaced the
dedicated NumberConstant and PhysicalConstant nodes. We also
demonstrated symbolic variable operations w/visualization and
end-node realization, validating that this approach to Expr sockets is
key.
We prepared for a new `BLPropType`, namely dynamic lists of dataclasses,
which will represent dynamic user-adjustable lists. This is the only
proper UI design for declaring symbols directly in an Expr node.
For now, we'll do without symbols, but will be a core feature for design
space exploration (aka. batch-run a bunch of sims), and for inverse
design.
Else, a few fixes: Naming of `ManagedBLImage` was updated, the expr
socket info display was fixed to display even when only output
information is available,
A few TODOs remain in the Expr Constant, but they are less important
before dynamic symbol declarations are in place.
We greatly enhanced the field, flux monitors, and added the permittivity
monitor.
mobj naming is still a bit borked; we need a node-tree bound namespace
to go further with it.
For now, don't duplicate nodes, and don't use multiple node trees.
Some rather foundational things were fundamentally broken, especially related to the initialization procedures of fields / cached properties.
- We completely revamped `bl_cache`, fixing many to-be-discovered bugs.
- We completely streamlined `BLField` property logic into reusable
`bl_cache.BLProp` and `bl_cache.BLPropType`.
- We implemented `BLInstance` superclass to handle ex. deterministic
persistance of dynamic enum items, and other nuanced common
functionality that was being duplicated raw.
- We implemented inter `cached_bl_property` / `BLField` dependency
logic, including the ability to invalidate dynamic enums without
@on_value_changed logic. This **greatly** simplifies a vast quantity
of nodes that were traditionally very difficult to get working due to
the sharp edges imposed by needing manual invalidation logic.
- We gave `ExprSocket` a significant usability upgrade, including
thorough parsing logic in the `SocketDef`.
It's not that existing nodes are as such broken, but their existing bugs
are now going to cause problems a lot faster.
Which is a good thing.
BREAKING CHANGE: Closes#13. Closes#16. Big work on #64. Work on #37.
We did it! We have a `Scene` node which outputs the current frame/time
mapped to a friendlier unit (like `ps`) and takes an integer FPS.
A `frame_post` handler in Blender makes sure to run `DataChanged` on any
`Scene` nodes in any active node trees (a notion we added with this
commit), which causes a `@property` to effectively masquerade as a
"real" property. Thus, all the caching/other stuff like that has its
semantics preserved, so long as the invalidated `@property` stays.
We can probably optimize the handler - it linearly scans all nodes in
all trees. A bit suboptimal. We can also think more about more outputs
and such. Still, a fine start, and very cool with videos of EM fields
doing their thing! (Of course, one can plug this into anything, not just
the time-fixing part of an Expr chain).
Closes#17.
It's recommended to add license headers to all files in an AGPL project, in case a file is viewed outside the context of its main repository. We're using a `pre-commit` tool to manage this, to make sure it's consistently applied to all our Python files.
This is it!
This is the milestone.
We can now make, run, and analyze simulations in one big chain.
Jank remains:
- Dynamic enums still need caching, lest the user think to restart
Blender while the math nodes are riding high on `FlowPending`.
- GN remains untested, and so forth.
- Still no plane wave node. Easy to jank together, though.
- Still no reindexing in the Transform math, so only frequencies for
now.
- Active kinds still don't update shape, we still need an explicit
(postinit?) directive to do that.
- No colors for expr sockets :(
But we have a beautifully solid foundation to work on.
The new abstractive tools for defining event-driven actions via nodes
have had very few showstoppers, and are incredibly nice to work with.
There are sharp edges, of course, but generally they only matter where
the problem was so very difficult to begin with.
We'll start doing physics immediately, and fixing bugs / implementing
more nodes as we go.
We've added enough nodes to run simulations, and there is now only an
"air gap" remaining between the math nodes and the sim design nodes.
Of particular importance right now are:
- Finalizing the exporter w/Sim Data output.
- Finishing the plane wave, as well as other key source nodes (TFSF,
Gaussian Beam, Plane Wave stand out in particular).
- Reduce node, as we need to be able to reconstruct total flux
through a field monitor, as well as generally do statistics.
- Transform node, particularly w/pure-InfoFlow reindexing (we need to be
able to reindex frequency -> vacwl), fourier transform (we need to be
able to cast a time-domain field recording to frequency domain).
- Finish debugging the basic structures, in particular the Cylinder
primitive, but also the generic GeoNodes node, so we can build the
membrane properly.
- Ah yes, also, the grid definition. The default can sometimes act a
little suspiciously.
It now supports selecting the variant, and exporting a generated
LazyArrayRange of valid freqs/wls.
The UI was also revamped, with greatly more readable range values,
dynamic labels, link button pointing to the original data, etc. .
While more LOC, the code structure is also far more explicit and predictable to
maintain.
A very healthy amount of research on how to choose the Bloch vector was
performed.
It is encapsulated not only in the documentation, but also in the modes
available for how to derive one that fits a given simulation.
The theory of the Bloch boundaries can really bite you, and the hope is
that by focusing on such invalid-usage-prevention, a lot of time can be
saved in the sim design stages.
It's useful for scenarios where we need to intersect geometry with the
simulation boundary.
Generally, not preferred, which we make clear in the docs :)
Also fixed a bug in `events.py` that was misjudging `FlowInitializing`,
and causing `BoundConds` to fail.
Weird.
Anyway, fixed!
We now have a solid category (w/accompanying sockets) for defining
boundary conditions.
We also have a single-boundary-condition node for fully configuring the
all-important PML condition.
Some insights:
- PEC/PMC are so dead-simple that giving them their own nodes doesn't
even make sense.
- StablePML and PML are the same, just with differing layers. Chose
to require the user add layers to the PML node for the same effect.
- "Periodic" is a special case of "Bloch", so we only need "Bloch".
- "Absorber" vs "PML" is an important choice for the user, which we
must ensure shines through.
I'm of the belief that the correct abstractions are now actually
available, and that most-to-all of the required functionality actually
already exists within the code base.
The art is bringing it together!
The performance difference isn't as clear cut as hoped.
However, the plotting procedure is enormously more straightforward, and
performance is more predictable.
So it's worth it.
We're managing to perfectly reuse figure/canvas/axis, but still hovering at around 70-80ms.
Mind you, the tested machine is an older laptop.
Still, things feel interactive enough, especially together with the
other modifications.
To really amp it up, we can look into blitting. It requires alterations
to the plotting methodology, but it offers a cached approach to drawing
only altered pixels (the bottleneck with `canvas.draw()` is that it
needs to render all the pixels, every time).
We can also try to lower the resolution if it's too slow.
Various small adjustments with a big total impact:
- Toggling 3D preview no longer propagates a DataChanged, which prevents
chronic `bl_select()` invocation that greatly slows down switching
between images.
- Viewer now uses a replot() context manager to hide any active plots
whenever no plots were generated, so that turning off plots /
previewing a node (chain) without plots properly turns off the image,
instead of letting the image hang around.
- `self.managed_objs` is now properly invalidated, instead of trying to
set the `name` attribute of in-memory objects, so that persistance
keeps up with `sim_node_name` changes, so that all the ex. 'Viz' nodes
don't all try to hog a single 'Viz' image name.
- A pre-save handler was added, which ensures all images are packed into
the .blend, to ensure that the images will pop up on the next file
load.
- A fake user is now assigned to all new images, to nail down the idea
that `ManagedBLImage` is the owner.
- `name` setter of `ManagedBLImage` was unreasonably bugged (it's
actually incredible that it worked) - it has been fixed, as well as
other changes applied to the class as a whole (including @classmethod
on the UI-area/space getters and minor None-sanitizing).
This was a nasty (interesting?) one - usually, input sockets are not attempted used
after the socket no longer exists.
Various checks in ex. `events` tend to help that process along.
Unfortunately (fortunately?), `Extract` uses a `_compute_input` query with
`optional=True`, which results in a direct attempt to hit the cache
without any other checks.
Because old input socket caches were never deleted, it would
**continue to get cached data from sockets that no longer exist**.
While on the surface this could be considered a case of "the
private method (`_compute_input`) is private for a reason", or
alternatively, "don't hijack the graph flow", I'm more convinced that
the usage is actually quite clean, being read-only and generally
well-conceived. It's reasonable to presume that asking for a thing that
doesn't exist won't produce output!
Moreover, I wouldn't be surprised if several other mysterious bugs were
caused by this. Not to mention the memory leak of endless caching! (Well,
until the node is deleted). It's a good things we noticed!
The Viz node now detects the shape of the data, and presents compatible
plot options.
Not all are implemented, but a few quite important ones are.
Additionally, a number of dataflow-related bugs were investigated and
fixed. A few were truly damaging, but many simply resulted in gross
inefficiencies - we must be careful declaring BLFields that are updated
in hot loops!
Moreover, it is exceptionally easy to add more as needed, as we analyze
more and more sims.
The only limit is `matplotlib`, which is... well, yeah.
Due to the BLField work, the dynamicness of the Viz node is quite
under control, so there will not be any critical issues there.
The plotting lags (70ms total in the hot loop), but that's actually
entirely fixeable.
It's also entirely the `managed_bl_image`'s fault.
Fixing these inefficiencies will also make Tidy3D's builtin plots
near-realtime, incidentally.
We profiled the following currently:
- 25ms: Creating `fig = plt.subplots`. We can reuse fig per-managed
image.
- 43ms: The BytesIO roundtrip, including `savefig`. We can instead use
the Agg backend, `fig.canvas.draw()`, and a `np.frombuffer` to both
plot directly to the memory location,
- ~3ms: Actual plotting functions in `image_ops`. They are seriously fast.
- ~0ms: Blitting pixels to the Blender image - this was optimized in
4.1, and it shows; the time to copy the data over is essentially nothing.