1+ # bluemira is an integrated inter-disciplinary design tool for future fusion
2+ # reactors. It incorporates several modules, some of which rely on other
3+ # codes, to carry out a range of typical conceptual fusion reactor design
4+ # activities.
5+ #
6+ # Copyright (C) 2021 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh,
7+ # J. Morris, D. Short
8+ #
9+ # bluemira is free software; you can redistribute it and/or
10+ # modify it under the terms of the GNU Lesser General Public
11+ # License as published by the Free Software Foundation; either
12+ # version 2.1 of the License, or (at your option) any later version.
13+ #
14+ # bluemira is distributed in the hope that it will be useful,
15+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17+ # Lesser General Public License for more details.
18+ #
19+ # You should have received a copy of the GNU Lesser General Public
20+ # License along with bluemira; if not, see <https://www.gnu.org/licenses/>.
21+ """
22+ Geometry Optimisation
23+
24+ Example taken from: bluemira/examples/optimisation/geometry_optimisation.ex.py
25+
26+ In this example we will go through how to set up a simple geometry
27+ optimisation, including a geometric constraint.
28+
29+ The problem to solve is, minimise the length of our wall boundary,
30+ in the xz-plane, whilst keeping it a minimum distance from our plasma.
31+
32+ We will greatly simplify this problem by working with a circular
33+ plasma, we will use a PrincetonD for the wall shape,
34+ and set the minimum distance to half a meter.
35+ """
36+
37+ import numpy as np
38+ import os
39+ import sys
40+ from bluemira .display import plot_2d
41+ from bluemira .display .plotter import PlotOptions
42+ from bluemira .geometry .optimisation import optimise_geometry
43+ from bluemira .geometry .parameterisations import GeometryParameterisation , PrincetonD
44+ from bluemira .geometry .tools import distance_to , make_circle
45+ from bluemira .geometry .wire import BluemiraWire
46+
47+ import simvue
48+
49+ def f_objective (geom : GeometryParameterisation ) -> float :
50+ """Objective function to minimise a shape's length."""
51+ return geom .create_shape ().length
52+
53+
54+ def distance_constraint (
55+ geom : GeometryParameterisation , boundary : BluemiraWire , min_distance : float , run : simvue .Run
56+ ) -> float :
57+ """
58+ A constraint to keep a minimum distance between two shapes.
59+
60+ The constraint must be in the form f(x) <= 0, i.e., constraint
61+ is satisfied if f(x) <= 0.
62+
63+ Since what we want is 'min_distance <= distance(A, B)', we rewrite
64+ this in the form 'min_distance - distance(A, B) <= 0', and return
65+ the left-hand side from this function.
66+ """
67+ shape = geom .create_shape ()
68+ # Log all variables as metrics after each iteration, giving human readable names:
69+ run .log_metrics (
70+ {
71+ "inboard_limb_radius" : float (geom .variables ["x1" ].value ),
72+ "outboard_limb_radius" : float (geom .variables ["x2" ].value ),
73+ "vertical_offset" : float (geom .variables ["dz" ].value ),
74+ "length_of_wall" : float (shape .length ),
75+ "distance_to_plasma" : float (distance_to (shape , boundary )[0 ])
76+ }
77+ )
78+ return min_distance - distance_to (shape , boundary )[0 ]
79+
80+ # The original example prints stuff to the console to track progress
81+ # Instead of changing these lines to log events (since we probably want both),
82+ # We can make a class which intercepts stdout and also sends messages to Simvue
83+ class StdoutToSimvue ():
84+ def __init__ (self , run : simvue .Run ):
85+ self .run = run
86+
87+ def write (self , message : str ):
88+ # Log the message as an event (so long as it isnt a blank line)
89+ if message .strip ():
90+ run .log_event (message )
91+ # And print to console as normal
92+ sys .__stdout__ .write (message )
93+
94+ def flush (self ):
95+ sys .__stdout__ .flush ()
96+
97+ # Here we will start doing our optimisation. First create a Simvue run,
98+ # using the Run class as a context manager:
99+ with simvue .Run () as run :
100+ # Initialise our run:
101+ run .init (
102+ name = "bluemira_geometry_optimisation" ,
103+ folder = "/simvue_client_demos" ,
104+ visibility = "tenant" if os .environ .get ("CI" ) else None ,
105+ tags = ["bluemira" , "simvue_client_examples" ],
106+ description = "Minimise the length of a parameterised geometry using gradient-based optimisation algorithm." ,
107+ )
108+
109+ # Redirect stdout so that print statements also get logged as events:
110+ stdout_sender = StdoutToSimvue (run )
111+ sys .stdout = stdout_sender
112+
113+ # Next define the shape of our plasma, and the minimum distance we want between
114+ # our wall boundary and our plasma:
115+ min_distance = 0.5
116+ plasma = make_circle (radius = 2 , center = (8 , 0 , 0.25 ), axis = (0 , 1 , 0 ))
117+
118+ # As with any optimisation, it's important to pick a reasonable initial
119+ # parameterisation.
120+ wall_boundary = PrincetonD ({
121+ "x1" : {"value" : 4 , "upper_bound" : 6 },
122+ "x2" : {"value" : 12 , "lower_bound" : 10 },
123+ })
124+
125+ print ("Initial parameterisation:" )
126+ print (wall_boundary .variables )
127+ print (f"Length of wall : { wall_boundary .create_shape ().length } " )
128+ print (f"Distance to plasma: { distance_to (wall_boundary .create_shape (), plasma )[0 ]} " )
129+
130+ # Create metadata for our original parameters:
131+ _metadata = {
132+ var : {
133+ "initial" : wall_boundary .variables [var ].value ,
134+ "lower_bound" : wall_boundary .variables [var ].lower_bound ,
135+ "upper_bound" : wall_boundary .variables [var ].upper_bound
136+ }
137+ for var in ["x1" , "x2" , "dz" ]
138+ }
139+ run .update_metadata ({"bluemira_parameters" : _metadata })
140+
141+ # Create and upload an image of the initial design to Simvue
142+ _plot = plot_2d ([wall_boundary .create_shape (), plasma ])
143+ _fig = _plot .get_figure ()
144+ run .save_object (_fig , category = "input" , name = "initial_shape" )
145+
146+ # Optimise our geometry using a gradient descent method
147+ result = optimise_geometry (
148+ wall_boundary ,
149+ algorithm = "SLSQP" ,
150+ f_objective = f_objective ,
151+ opt_conditions = {"ftol_abs" : 1e-6 },
152+ keep_history = True ,
153+ ineq_constraints = [
154+ {
155+ "f_constraint" : lambda g : distance_constraint (g , plasma , min_distance , run ),
156+ "tolerance" : np .array ([1e-8 ]),
157+ },
158+ ],
159+ )
160+
161+ # Print final results after optimisation
162+ print ("Optimised parameterisation:" )
163+ print (result .geom .variables )
164+
165+ boundary = result .geom .create_shape ()
166+ print (f"Length of wall : { boundary .length } " )
167+ print (f"Distance to plasma: { distance_to (boundary , plasma )[0 ]} " )
168+
169+ # Update metadata with final optimised values
170+ _metadata = {
171+ var : {
172+ "final" : result .geom .variables [var ].value ,
173+ }
174+ for var in ["x1" , "x2" , "dz" ]
175+ }
176+ run .update_metadata ({"bluemira_parameters" : _metadata })
177+
178+ # Create and upload an image of the optimised design to Simvue
179+ _plot = plot_2d ([boundary , plasma ])
180+ _fig = _plot .get_figure ()
181+ run .save_object (_fig , category = "output" , name = "final_shape" )
182+
183+ # Use the history to create and upload an image of the design iterations
184+ geom = PrincetonD ()
185+ ax = plot_2d (plasma , show = False )
186+ for i , (x , _ ) in enumerate (result .history ):
187+ geom .variables .set_values_from_norm (x )
188+ wire = geom .create_shape ()
189+ wire_options = {
190+ "alpha" : 0.5 + ((i + 1 ) / len (result .history )) / 2 ,
191+ "color" : "red" ,
192+ "linewidth" : 0.1 ,
193+ }
194+ ax = plot_2d (wire , options = PlotOptions (wire_options = wire_options ), ax = ax , show = False )
195+ _plot = plot_2d (boundary , ax = ax , show = True )
196+ _fig = _plot .get_figure ()
197+ run .save_object (_fig , category = "output" , name = "design_iterations" )
0 commit comments